diff --git a/config.json.example b/config.json.example
index d8bb67b62e..6933f92426 100644
--- a/config.json.example
+++ b/config.json.example
@@ -21,6 +21,7 @@
"NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID",
"NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY",
"GA_ID": "GA_ID",
+ "FLAG_REPORT_EMAIL": "email@mod.com",
"EMAIL_SERVER": {
"url": "http://example.com",
"authUser": "user",
@@ -33,13 +34,22 @@
},
"PAYPAL":{
"billing_plans": {
- "1":"1",
- "3":"3",
- "6":"6",
- "12":"12"
+ "basic_earned":"basic_earned",
+ "basic_3mo":"basic_3mo",
+ "basic_6mo":"basic_6mo",
+ "google_6mo":"google_6mo",
+ "basic_12mo":"basic_12mo"
},
"mode":"sandbox",
"client_id":"client_id",
"client_secret":"client_secret"
+ },
+ "IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
+ "LOGGLY": {
+ "enabled": false,
+ "subdomain": "subdomain",
+ "token": "token",
+ "username": "username",
+ "password": "password"
}
}
diff --git a/keys/google/iap-live b/keys/google/iap-live
new file mode 100644
index 0000000000..69dc38136b
--- /dev/null
+++ b/keys/google/iap-live
@@ -0,0 +1 @@
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk6EVccD7D6y5vLmsUxUXVYTMyByq+NjfTzjMBaKCk93fq8Ahn6Ipha1K4oBIvArUj3CsP5Tb/Yf6c6SrEsSnE3eqgKQgeq+fYyfIm+Fb+tFGTttLonZ2Sqemqeyx2PjOG2CVZBXj1+Zr7jb0ocMNYraYUTvklWnpcK6/5ZaZLLent1PGiU9l5jIfRAsxmJH/UpssgpFPnCs+8PRKUALkJATTfVCCF+PJef6mcKAafAArKUEo0lR2/NeaE62Wpii33DLuKDPfMqL3g4EHZTEVE1ezFl/3WPeLlxuQm38hZLpEfEYKp17qzplU+GmWIfjZt6OdtVdonlYxIxNAZLnWEQIDAQAB
\ No newline at end of file
diff --git a/migrations/20141211_NaN_consecutives.js b/migrations/20141211_NaN_consecutives.js
new file mode 100644
index 0000000000..2822df0267
--- /dev/null
+++ b/migrations/20141211_NaN_consecutives.js
@@ -0,0 +1,4 @@
+db.users.update({'purchased.plan.consecutive.count':NaN}, {$set:{'purchased.plan.consecutive.count':0}}, {multi:1});
+db.users.update({'purchased.plan.consecutive.offset':NaN}, {$set:{'purchased.plan.consecutive.offset':0}}, {multi:1});
+db.users.update({'purchased.plan.consecutive.gemCapExtra':NaN}, {$set:{'purchased.plan.consecutive.gemCapExtra':0}}, {multi:1});
+db.users.update({'purchased.plan.consecutive.trinkets':NaN}, {$set:{'purchased.plan.consecutive.trinkets':0}}, {multi:1});
\ No newline at end of file
diff --git a/migrations/20141230_new_years_hats.js b/migrations/20141230_new_years_hats.js
new file mode 100644
index 0000000000..595027be0a
--- /dev/null
+++ b/migrations/20141230_new_years_hats.js
@@ -0,0 +1,11 @@
+db.users.update(
+ {'items.gear.owned.head_special_nye':{$ne:null}},
+ {$set:{'items.gear.owned.head_special_nye2014':false}},
+ {multi:1}
+)
+
+db.users.update(
+ {'items.gear.owned.head_special_nye':null},
+ {$set:{'items.gear.owned.head_special_nye':false}},
+ {multi:1}
+)
diff --git a/migrations/mysteryitems.js b/migrations/mysteryitems.js
index b222047bda..47437570ef 100644
--- a/migrations/mysteryitems.js
+++ b/migrations/mysteryitems.js
@@ -2,7 +2,7 @@ var _id = '';
var update = {
$push: {
'purchased.plan.mysteryItems':{
- $each:['head_mystery_201411','weapon_mystery_201411']
+ $each:['head_mystery_201412','armor_mystery_201412']
}
}
};
diff --git a/package.json b/package.json
index fc3edb043b..6f14a60e51 100644
--- a/package.json
+++ b/package.json
@@ -26,9 +26,11 @@
"grunt-nodemon": "~0.3.0",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop",
"icalendar": "git://github.com/lefnire/node-icalendar#master",
+ "in-app-purchase": "^0.2.0",
"jade": "~1.7.0",
"js2xmlparser": "~0.1.2",
"lodash": "~2.4.1",
+ "loggly": "~1.0.8",
"method-override": "~2.2.0",
"moment": "~2.8.3",
"mongoose": "~3.8.17",
diff --git a/public/css/avatar.styl b/public/css/avatar.styl
index f5132b070f..13bd58246d 100644
--- a/public/css/avatar.styl
+++ b/public/css/avatar.styl
@@ -30,17 +30,19 @@ future re: pets and whatnot, this is just temporary.
.avatar-level, .avatar-name
position: absolute
@extend $hrpg-label
+ z-index: 2
.avatar-level
- bottom: 2px
- right: 2px
+ bottom: 4px
+ right: 4px
// when a buff/rebirth exists, add space between the icon and the level
.glyphicon-circle-arrow-up
padding-right: 4px
.glyphicon-plus-sign
padding-left: 4px
.avatar-name
- top: 2px
- left: 2px
+ max-width: 132px
+ top: 4px
+ left: 4px
opacity: 0 // Hide username by default
transition: opacity 0.2s ease-out
@@ -83,8 +85,9 @@ future re: pets and whatnot, this is just temporary.
width: 50px !important
.addthis_native_toolbox
position: absolute
- top:2px
- right:2px
+ bottom:4px
+ left:4px
+ z-index: 2
opacity:0
&:hover .addthis_native_toolbox
opacity:1
diff --git a/public/css/game-pane.styl b/public/css/game-pane.styl
index 1a002370ba..8ccc3c211d 100644
--- a/public/css/game-pane.styl
+++ b/public/css/game-pane.styl
@@ -37,6 +37,9 @@
markdown, span.time
padding: 0 3px
line-height: 1.618em
+
+ .long-title .tooltip
+ white-space: normal
span.time
white-space: nowrap
diff --git a/public/css/global-modules.styl b/public/css/global-modules.styl
index cdac07da4b..0629a881df 100644
--- a/public/css/global-modules.styl
+++ b/public/css/global-modules.styl
@@ -71,7 +71,7 @@ hrpg-button-color-mixin($hrpg-button-color)
color: #fff !important;
$hrpg-button-master
list-style: none
- > a, > button, > input
+ > a, > button, > input, label::after
display: inline-block !important
padding: 0.25em 0.5em !important
text-decoration:none
@@ -86,7 +86,7 @@ $hrpg-button-master
background-color: #aaa !important
$hrpg-button
@extend $hrpg-button-master
- > a, > button, > input
+ > a, > button, > input, label::after
border: 1px solid #ccc !important
border-radius: 0.382em !important
$hrpg-button-call-to-action
@@ -111,7 +111,7 @@ $hrpg-button-with-input
height: 2em
input
border: 1px solid #ccc
- border-radius: 0.382em 0em 0em 0.382em
+ border-radius: 0.382em 0em 0em 0.382em !important
padding-left: 0.618em
background-color: #fff !important
-webkit-appearance: none
@@ -150,15 +150,17 @@ $hrpg-button-bar
border-right: 1px solid darken(#fff,3.82%)
float:left
word-wrap: break-word
+ button
+ border: none
li:nth-of-type(2)
border-radius: 0.382em 0em 0em 0.382em
li:first-of-type
border-radius: 0.382em 0em 0em 0.382em
- a
+ a, button
border-radius: 0.382em 0em 0em 0.382em
li:last-of-type
border-right: none
- a
+ a, button
border-radius: 0em 0.382em 0.382em 0em
$hrpg-button-bar-with-heading
diff --git a/public/css/helpers.styl b/public/css/helpers.styl
index 52d245a3fb..d9b54a3089 100644
--- a/public/css/helpers.styl
+++ b/public/css/helpers.styl
@@ -32,4 +32,33 @@
visibility: hidden
.transparent
- opacity: 0.5
\ No newline at end of file
+ opacity: 0.5
+
+// Allow block element to be centered
+.col-centered
+ margin-left: auto
+ margin-right: auto
+
+// .pull-right & .pull-left for specific screen sizes
+// Follows conventions for col-x-SIZE classes
+
+@media (min-width: 768px)
+ .pull-left-sm
+ float: left
+
+ .pull-right-sm
+ float: right
+
+@media (min-width: 992px)
+ .pull-left-md
+ float: left
+
+ .pull-right-md
+ float: right
+
+@media (min-width: 1200px)
+ .pull-left-lg
+ float: left
+
+ .pull-right-lg
+ float: right
diff --git a/public/css/index.styl b/public/css/index.styl
index 07e2829d0b..d7e50fb345 100644
--- a/public/css/index.styl
+++ b/public/css/index.styl
@@ -60,6 +60,8 @@ html, body
@media (max-width: 600px)
#wrap
margin-top: 0
+ padding: 0 4px;
+ overflow-x: hidden;
/* Gems
-------------------------------------------------- */
@@ -135,6 +137,9 @@ a
a span.glyphicon
color: #333 // Takes base color from bootstrap
+ &.text-danger
+ color: #a94442 // Allows glyphicon to use text-danger class
+
a.label
color: #fff
@@ -176,3 +181,5 @@ a.label
.white, .white a
color: #fff !important
+.line-through
+ text-decoration line-through
diff --git a/public/css/items.styl b/public/css/items.styl
index 35dd7e7ef2..213e499835 100644
--- a/public/css/items.styl
+++ b/public/css/items.styl
@@ -4,7 +4,7 @@
line-height: 1.5em
padding-left: 0.75em
-.shop_gold, .shop_silver, .shop_copper
+.shop_gold, .shop_silver, .shop_copper, .shop_spell
vertical-align: top
display: inline-block
diff --git a/public/css/quests.styl b/public/css/quests.styl
index fe7209f4e9..3f5fa8a79b 100644
--- a/public/css/quests.styl
+++ b/public/css/quests.styl
@@ -1,3 +1,7 @@
.quest_collected_true
color: #ccc
- text-decoration:line-through
\ No newline at end of file
+ text-decoration:line-through
+
+quest-rewards
+ hr
+ clear:both
diff --git a/public/css/shared.styl b/public/css/shared.styl
index 71e7f9f48a..d05ebcbb95 100644
--- a/public/css/shared.styl
+++ b/public/css/shared.styl
@@ -48,5 +48,3 @@ a.hint:hover
.tasks-lists .row .col-md-3
padding-right: 0
padding-left: 0
- .task-column
- padding: 0
diff --git a/public/css/tasks.styl b/public/css/tasks.styl
index 856b7a9a5e..7e50dfbcf3 100644
--- a/public/css/tasks.styl
+++ b/public/css/tasks.styl
@@ -8,29 +8,99 @@ $stages = (worst $worst) (worse $worse) (bad $bad) (neutral $neutral) (good $goo
// for each color stage, generate a named class w/ the appropriate color
for $stage in $stages
- .color-{$stage[0]}
- background-color: $stage[1]
- .action-yesno label,
- .task-action-btn
- background-color: darken($stage[1], 30%)
- &:hover, &:focus
- background-color: darken($stage[1], 40%)
- .task-options
+ .task-column:not(.rewards)
+ .color-{$stage[0]}:not(.completed)
background-color: $stage[1]
-
+ border: 1px solid shade($stage[1],10%)
+ .priority-multiplier, .task-attributes, .repeat-days
+ li
+ hrpg-button-color-mixin($stage[1])
+ button
+ &.active
+ box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
+ background-color: darken($stage[1],5%) !important
+ &:focus
+ border: 1px solid darken($stage[1],30%)
+ outline: 0
+ .plusminus
+ .task-checker
+ label:after
+ border: 1px solid darken($stage[1], 30%) !important
+ input[type=checkbox]:checked + label:after
+ //border: 1px solid darken($stage[1], 50%) !important
+ box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
+ background-color: darken($stage[1],5%) !important
+ .save-close, .task-checklist-edit li
+ hrpg-button-color-mixin(darken($stage[1],5%))
+ button
+ &:focus
+ border: 1px solid darken($stage[1],30%)
+ outline: 0
+ .action-yesno label,
+ .task-action-btn
+ background-color: darken($stage[1], 30%)
+ &:hover, &:focus
+ background-color: darken($stage[1], 40%)
+ .task-options
+ background-color: $stage[1]
+ .option-group:not(.task-checklist)
+ border-bottom: 1px solid darken($stage[1], 15%)
+ .option-content
+ border-color: darken($stage[1], 16.18%) !important
+ &:hover
+ border-color: darken($stage[1], 32.8%) !important
+ &:focus
+ border-color: darken($stage[1], 61.8%) !important
+ outline: none;
+
// completed has to be outside the loop to override the color class
.completed
+ color: darken($completed,30%)
background-color: $completed
- color: #999
- .glyphicon
- color: #333
- .task-checker label
- background-color: darken($completed, 30%)
+ border: 1px solid shade($completed,10%)
+ .priority-multiplier, .task-attributes, .repeat-days
+ li
+ hrpg-button-color-mixin($completed)
+ button
+ &.active
+ box-shadow: inset 0 0 0 1px darken($completed,40%) !important
+ background-color: darken($completed,5%) !important
+ &:focus
+ border: 1px solid darken($completed,30%)
+ outline: 0
+ .plusminus
+ .task-checker
+ label:after
+ border: 1px solid darken($completed, 30%) !important
+ input[type=checkbox]:checked + label:after
+ box-shadow: inset 0 0 0 1px darken($completed,40%) !important
+ background-color: darken($completed,5%) !important
+ .save-close, .task-checklist-edit li
+ hrpg-button-color-mixin(darken($completed,5%))
+ button
+ &:focus
+ border: 1px solid darken($completed,30%)
+ outline: 0
+ .action-yesno label,
.task-action-btn
background-color: darken($completed, 30%)
+ &:hover, &:focus
+ background-color: darken($completed, 40%)
+ .task-options
+ background-color: $completed
+ .option-group:not(.task-checklist)
+ border-bottom: 1px solid darken($completed, 15%)
+ .option-content
+ border-color: darken($completed, 16.18%) !important
+ &:hover
+ border-color: darken($completed, 32.8%) !important
+ &:focus
+ border-color: darken($completed, 61.8%) !important
+ outline: 0;
-.reward
+.reward, .reward-item
background-color: white
+ border: 1px solid $completed !important
// 2. Columns & Tasks
// ===================
@@ -70,7 +140,7 @@ for $stage in $stages
// "Add new task" form
// -----------------
.task-add
- margin-top: 1.618em
+ margin-top: 1.5em
@extend $hrpg-button-with-input
hrpg-button-color-mixin($color-options-submenu)
input, button
@@ -90,7 +160,7 @@ for $stage in $stages
padding: 0
min-height: 3em
margin-bottom: 0.75em
- box-shadow: inset 0 0 1px rgba(0,0,0,0.5)
+ // box-shadow: inset 0 0 1px rgba(0,0,0,0.5)
position: relative
&:hover
cursor: move
@@ -99,6 +169,16 @@ for $stage in $stages
label
font-weight: 400
+
+ input[type="text"], input[type="number"], textarea.option-content
+ border: 1px solid #aaa
+ border-radius: 0.382em
+ padding-left: 0.618em
+ background-color: #fff !important
+ -webkit-appearance: none
+ -moz-appearance: none
+ appearance: none
+ box-shadow: none //remove red glow in Firefox
// task content
.task-text
@@ -107,7 +187,7 @@ for $stage in $stages
line-height: 1.4
word-wrap: break-word
- markdown > ul > li // Otherwise, + gets turned into a bullet and gets maksed by buttons
+ markdown > ul > li // Otherwise, + gets turned into a bullet and gets masked by buttons
margin-left:20px
span.emoji
@@ -118,6 +198,8 @@ for $stage in $stages
.habit-wide .task-text
padding-left: 7em
+.habit-narrow .task-text
+ padding-left: 0.75em
// when a task is being dragged
.task.ui-sortable-helper
@@ -174,28 +256,21 @@ for $stage in $stages
cursor: pointer
// plus/minus checkbox
-.action-plusminus
- label
- margin-right: 1.5em
- label:after
- font-size: 1.41em
- width: 2.12765em
- height: 2.12765em
- line-height: 2.12765
- text-align: center
- display: inline-block
- vertical-align top
- opacity: 0.3
- background-color: #ddd
- outline: 1px solid rgba(0,0,0,0.5)
- color: #222
- label[for$="plus"]:after
- content: '+'
- label[for$="minus"]:after
- content: '−'
-
- &.select-toggle input[type=checkbox]:checked + label:after
- opacity: 1
+.plusminus
+ text-align: center
+ .task-checker
+ font-size: 1.5em // 1.41em
+ @extend $hrpg-button
+ margin-bottom: 0.5em
+ &:first-of-type
+ margin-right: 0.5em
+ label:after
+ width: 2em
+ line-height: 1.5
+ label[for$="plus"]:after
+ content: '+'
+ label[for$="minus"]:after
+ content: '−'
// yes/no checkbox
.action-yesno
@@ -227,9 +302,9 @@ for $stage in $stages
label:hover:after,
label:focus:after
content: "\E013"
- font-family 'Glyphicons Halflings'
- border none
- margin 0
+ font-family: "Glyphicons Halflings"
+ border: none
+ margin: 0
line-height: 1.714285714em
height: 1.714285714em
width: 1.714285714em
@@ -241,9 +316,9 @@ for $stage in $stages
// completed icon
input[type=checkbox]:checked + label:after
content: "\E013"
- font-family 'Glyphicons Halflings'
- border none
- margin 0
+ font-family: "Glyphicons Halflings"
+ border: none
+ margin: 0
line-height: 1.714285714em
height: 1.714285714em
width: 1.714285714em
@@ -295,36 +370,36 @@ form
//background-color:
.option-group
- border-bottom: 1px solid rgba(0,0,0,0.1)
+ //border-bottom: 1px solid rgba(0,0,0,0.1)
padding: 0 0 1em
margin-bottom: 1em
.option-title
font-size: 1em
margin: 0 0 0.5em
- line-height: 1.5
+ line-height: 2.5
border: 0
padding: 0
- color: #333
- font-style: italic
- &:not(.mega):after
- content: ':'
+ //color: #333
+ //font-style: italic
+ font-weight: bold
+ text-align: center
&.mega
cursor: pointer
- font-style: normal
- font-weight: bold
- text-decoration: underline
+ //font-style: normal
+ &:after
+ font-family: "Glyphicons Halflings"
+ font-size: 0.75em
+ content: "\E114"
+ padding-left: 0.75em
+ //&:not(.mega):after
+ //content: ':'
.option-content
height: 2.5em
width: 100%
margin: 0 0 1em
padding: 0 0 0 0.5em
- outline: 0
- border: 1px solid rgba(0,0,0,0.2)
- box-shadow: none
- border-radius: 0
- font-family: 'Lato', sans-serif //bootstrap override
textarea.option-content
height: 5em
@@ -393,47 +468,66 @@ form
.task-options
.task-action-btn.tile
margin: 3px
-
+
+ form
+ padding-bottom: 1em
+ .priority-multiplier, .task-attributes, .repeat-days
+ text-align: center
+ li
+ @extend $hrpg-button
+ margin-bottom: 1em
+ margin-right: 0.75em
+ display: inline-block
+ &:last-of-type
+ margin-right: 0
+ .repeat-days
+ li
+ button
+ min-width: 2.5em
+ .save-close
+ text-align: center
+ @extend $hrpg-button
// todos ui
// --------
// Checklists
// --------
-.checklist-form
- /* Make an input blend into its parent */
- input.inline-edit
- reset-box-model()
- reset-font()
- line-height: inherit
- text-align: inherit
- background-color:inherit
- box-shadow: none
- width: 80%
+.task-checklist-edit
+ > ul
+ text-align: center
+ li
+ @extend $hrpg-button
+ button
+ span
+ margin-right: 0.5em
+ > form
+ li
+ @extend $hrpg-button-with-input
+ @extend $clearfix
+ input
+ width: 70%
- /* Add interaction cues on hover and focus */
- input.inline-edit:hover,input.inline-edit:focus
- background-color: #FFD
- transition: background-color 0.5s
+ /* Add interaction cues on hover and focus */
+ &:hover,&:focus
+ background-color: #FFD
+ transition: background-color 0.5s
- .checklist-icons
- opacity:0
- width: 5%
-
- li
- min-height: 20px
- padding-left: 10px
- margin-bottom: 5px
-
- [type="checkbox"]
- margin: 0px 10px 3px 0px
- width: 4%
- li:hover .checklist-icons
- opacity:1
+ .checklist-icon
+ opacity:0.25
+ text-align: center
+ line-height: 1.5
+ float: left
+ padding: 0.25em 0.5em
+
+ li
+ margin-bottom: 1em
+ &:hover .checklist-icon
+ opacity:1
// Task filters
// --------
.task-filter
- margin: 1.618em 0 1em 0
+ margin: 1.5em 0 1em 0
hrpg-button-bar-mixin($color-tasks)
@extend $hrpg-button-bar
width: 100%
@@ -449,16 +543,18 @@ form
width: 100%
height: 100%
.rewards
+ .task
+ min-height: 3.07em // Temp fix for slight misalignment of btn-buy
.task-filter
li:nth-child(3n + 1):nth-last-child(2)
width: 33.333%
li:nth-child(3n + 1):nth-last-child(2) + li
width: 66.666%
-
.btn-buy
word-wrap: break-word
padding-left: 0.2em
text-align: left
+ border:none
span
position: relative
left: -2px
@@ -474,9 +570,4 @@ form
margin: auto
.shop_gold + .reward-cost
line-height: 1
- top: -0.1em
-
-.repeat-days
- a
- border-radius: 0.382em
-
+ top: -0.1em
diff --git a/public/js/app.js b/public/js/app.js
index c12ba3aa08..d97c3d3980 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -7,7 +7,6 @@ window.habitrpg = angular.module('habitrpg',
// @see https://github.com/angular-ui/ui-router/issues/110 and https://github.com/HabitRPG/habitrpg/issues/1705
// temporary hack until they have a better solution
- .value('$anchorScroll', angular.noop)
.constant("API_URL", "")
.constant("STORAGE_USER_ID", 'habitrpg-user')
@@ -185,6 +184,10 @@ window.habitrpg = angular.module('habitrpg',
url: '/timetravelers',
templateUrl: "partials/options.inventory.timetravelers.html"
})
+ .state('options.inventory.seasonalshop', {
+ url: '/seasonalshop',
+ templateUrl: "partials/options.inventory.seasonalshop.html"
+ })
// Options > Settings
.state('options.settings', {
diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js
index 9017602aec..4a42b10952 100644
--- a/public/js/controllers/footerCtrl.js
+++ b/public/js/controllers/footerCtrl.js
@@ -42,6 +42,7 @@
//$.getScript("//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4"); //FIXME why isn't this working when here? instead it's now in
var addthisServices = 'facebook,twitter,googleplus,tumblr,'+window.env.BASE_URL.replace('https://','').replace('http://','');
window.addthis_config = {
+ ui_click: true,
services_custom:{
name: "Download",
url: window.env.BASE_URL+"/export/avatar-"+User.user._id+".png",
diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js
index a9f744839c..fe30af9239 100644
--- a/public/js/controllers/groupsCtrl.js
+++ b/public/js/controllers/groupsCtrl.js
@@ -108,8 +108,8 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}
])
- .controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification',
- function($scope, $rootScope, Members, Shared, $http, Notification) {
+ .controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification', 'Groups',
+ function($scope, $rootScope, Members, Shared, $http, Notification, Groups) {
$scope.timestamp = function(timestamp){
return moment(timestamp).format('MM/DD/YYYY');
}
@@ -129,7 +129,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.gift = {
type: 'gems',
gems: {amount:0, fromBalance:true},
- subscription: {months:0},
+ subscription: {key:''},
message:''
};
$scope.sendGift = function(uuid, gift){
@@ -139,6 +139,20 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.$close();
})
}
+ $scope.reportAbuse = function(reporter, message, groupId) {
+ message.flags[reporter._id] = true;
+ Groups.Group.flagChatMessage({gid: groupId, messageId: message.id}, undefined, function(data){
+ Notification.text(window.env.t('abuseReported'));
+ $scope.$close();
+ });
+ }
+ $scope.clearFlagCount = function(message, groupId) {
+ Groups.Group.clearFlagCount({gid: groupId, messageId: message.id}, undefined, function(data){
+ message.flagCount = 0;
+ Notification.text("Flags cleared");
+ $scope.$close();
+ });
+ }
}
])
@@ -197,7 +211,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
});
}])
- .controller('ChatCtrl', ['$scope', 'Groups', 'User', '$http', 'ApiUrlService', 'Notification', function($scope, Groups, User, $http, ApiUrlService, Notification){
+ .controller('ChatCtrl', ['$scope', 'Groups', 'User', '$http', 'ApiUrlService', 'Notification', 'Members', '$rootScope', function($scope, Groups, User, $http, ApiUrlService, Notification, Members, $rootScope){
$scope.message = {content:''};
$scope._sending = false;
@@ -239,12 +253,14 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.deleteChatMessage = function(group, message){
if(message.uuid === User.user.id || (User.user.backer && User.user.contributor.admin)){
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
- Groups.Group.deleteChatMessage({gid:group._id, messageId:message.id, previousMsg:previousMsg}, undefined, function(data){
- if(data.chat) group.chat = data.chat;
+ if(confirm('Are you sure you want to delete this message?')){
+ Groups.Group.deleteChatMessage({gid:group._id, messageId:message.id, previousMsg:previousMsg}, undefined, function(data){
+ if(data.chat) group.chat = data.chat;
- var i = _.findIndex(group.chat, {id: message.id});
- if(i !== -1) group.chat.splice(i, 1);
- });
+ var i = _.findIndex(group.chat, {id: message.id});
+ if(i !== -1) group.chat.splice(i, 1);
+ });
+ }
}
}
@@ -262,6 +278,22 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$http.post(ApiUrlService.get() + '/api/v2/groups/' + group._id + '/chat/' + message.id + '/like');
}
+ $scope.flagChatMessage = function(groupId,message) {
+ if(!message.flags) message.flags = {};
+ if(message.flags[User.user._id])
+ Notification.text(window.env.t('abuseAlreadyReported'));
+ else {
+ $scope.abuseObject = message;
+ $scope.groupId = groupId;
+ Members.selectMember(message.uuid, function(){
+ $rootScope.openModal('abuse-flag',{
+ controller:'MemberModalCtrl',
+ scope: $scope
+ });
+ });
+ }
+ }
+
$scope.sync = function(group){
group.$get();
}
diff --git a/public/js/controllers/inventoryCtrl.js b/public/js/controllers/inventoryCtrl.js
index c302de46d1..eefee1b4df 100644
--- a/public/js/controllers/inventoryCtrl.js
+++ b/public/js/controllers/inventoryCtrl.js
@@ -88,16 +88,23 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
}
$scope.purchase = function(type, item){
- if (item.key=='spookDust') return User.user.ops.buySpookDust({});
+ if (type == 'special') return User.user.ops.buySpecialSpell({params:{key:item.key}});
var gems = User.user.balance * 4;
- if(gems < item.value) return $rootScope.openModal('buyGems');
- var string = (type == 'hatchingPotions') ? window.env.t('hatchingPotion') : (type == 'eggs') ? window.env.t('eggSingular') : (type == 'quests') ? window.env.t('quest') : (item.key == 'Saddle') ? window.env.t('foodSaddleText').toLowerCase() : (type == 'special') ? item.key : type; // this is ugly but temporary, once the purchase modal is done this will be removed
- var message = window.env.t('buyThis', {text: string, price: item.value, gems: gems})
+ var string = (type == 'weapon') ? window.env.t('weapon') : (type == 'armor') ? window.env.t('armor') : (type == 'head') ? window.env.t('headgear') : (type == 'shield') ? window.env.t('offhand') : (type == 'hatchingPotions') ? window.env.t('hatchingPotion') : (type == 'eggs') ? window.env.t('eggSingular') : (type == 'quests') ? window.env.t('quest') : (item.key == 'Saddle') ? window.env.t('foodSaddleText').toLowerCase() : type; // this is ugly but temporary, once the purchase modal is done this will be removed
+ if (type == 'weapon' || type == 'armor' || type == 'head' || type == 'shield') {
+ if (gems < ((item.specialClass == "wizard") && (item.type == "weapon")) + 1) return $rootScope.openModal('buyGems');
+ var message = window.env.t('buyThis', {text: string, price: ((item.specialClass == "wizard") && (item.type == "weapon")) + 1, gems: gems})
+ if($window.confirm(message))
+ User.user.ops.purchase({params:{type:"gear",key:item.key}});
+ } else {
+ if(gems < item.value) return $rootScope.openModal('buyGems');
+ var message = window.env.t('buyThis', {text: string, price: item.value, gems: gems})
+ if($window.confirm(message))
+ User.user.ops.purchase({params:{type:type,key:item.key}});
+ }
- if($window.confirm(message))
- User.user.ops.purchase({params:{type:type,key:item.key}});
}
$scope.choosePet = function(egg, potion){
@@ -157,5 +164,23 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
$rootScope.selectedQuest = item;
$rootScope.openModal('buyQuest', {controller:'InventoryCtrl'});
}
+
+ $scope.getSeasonalShopArray = function(set){
+ var flatGearArray = _.toArray(Content.gear.flat);
+
+ var filteredArray = _.where(flatGearArray, {index: set});
+
+ return filteredArray;
+ };
+
+ $scope.getSeasonalShopQuests = function(set){
+ var questArray = _.toArray(Content.quests);
+
+ var filteredArray = _.filter(questArray, function(q){
+ return q.key == "evilsanta" || q.key == "evilsanta2";
+ });
+
+ return filteredArray;
+ };
}
]);
diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js
index cbce9d31fb..5065a93eb2 100644
--- a/public/js/controllers/rootCtrl.js
+++ b/public/js/controllers/rootCtrl.js
@@ -195,6 +195,9 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$scope.castStart = function(spell) {
if (User.user.stats.mp < spell.mana) return Notification.text(window.env.t('notEnoughMana'));
+ if (spell.key == 'nye' && User.user.stats.gp < spell.value)
+ return Notification.text('Not enough gold.');
+
$rootScope.applyingAction = true;
$scope.spell = spell;
if (spell.target == 'self') {
@@ -236,7 +239,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$scope.spell = null;
}
- // Becuase our angular-ui-router uses anchors for urls (/#/options/groups/party), window.location.href=... won't
+ // Because our angular-ui-router uses anchors for urls (/#/options/groups/party), window.location.href=... won't
// reload the page. Perform manually.
$rootScope.hardRedirect = function(url){
window.location.href = url;
diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js
index 0c0123d1f3..a7927d5d0c 100644
--- a/public/js/controllers/settingsCtrl.js
+++ b/public/js/controllers/settingsCtrl.js
@@ -65,6 +65,8 @@ habitrpg.controller('SettingsCtrl',
User.set({'preferences.language': $scope.language.code});
}
+ $scope.availableFormats = ['MM/dd/yyyy','dd/MM/yyyy', 'yyyy/MM/dd'];
+
$scope.reroll = function(){
User.user.ops.reroll({});
$rootScope.$state.go('tasks');
@@ -179,5 +181,15 @@ habitrpg.controller('SettingsCtrl',
$scope.deleteWebhook = function(id) {
User.user.ops.deleteWebhook({params:{id:id}});
}
+
+ $scope.applyCoupon = function(coupon){
+ $http.get(ApiUrlService.get() + '/api/v2/coupons/valid-discount/'+coupon)
+ .success(function(){
+ Notification.text("Coupon applied!");
+ var subs = $scope.Content.subscriptionBlocks;
+ subs["basic_6mo"].discount = true;
+ subs["google_6mo"].discount = false;
+ });
+ }
}
]);
diff --git a/public/js/controllers/tasksCtrl.js b/public/js/controllers/tasksCtrl.js
index 2ff173dc63..fd8bcfe0a9 100644
--- a/public/js/controllers/tasksCtrl.js
+++ b/public/js/controllers/tasksCtrl.js
@@ -108,7 +108,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
*/
function focusChecklist(task,index) {
window.setTimeout(function(){
- $('#task-'+task.id+' .checklist-form .inline-edit')[index].focus();
+ $('#task-'+task.id+' .checklist-form input[type="text"]')[index].focus();
});
}
$scope.addChecklist = function(task) {
diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js
index 42dd787846..e07f21b15c 100644
--- a/public/js/services/groupServices.js
+++ b/public/js/services/groupServices.js
@@ -13,6 +13,8 @@ angular.module('groupServices', ['ngResource']).
//query: {method: "GET", isArray:false},
postChat: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat'},
deleteChatMessage: {method: "DELETE", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId'},
+ flagChatMessage: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId/flag'},
+ clearFlagCount: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId/clearflags'},
join: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/join'},
leave: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/leave'},
invite: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/invite'},
diff --git a/public/js/services/paymentServices.js b/public/js/services/paymentServices.js
index 4fc1b24e84..3a0f9b21da 100644
--- a/public/js/services/paymentServices.js
+++ b/public/js/services/paymentServices.js
@@ -3,23 +3,12 @@
angular.module('paymentServices',[]).factory('Payments',
['$rootScope', 'User', '$http', 'Content',
function($rootScope, User, $http, Content) {
- var user = User.user;
- var plan = User.user.purchased.plan;
var Payments = {};
- Payments.currentSub = _.find(Content.subscriptionBlocks, function(b){
- switch (plan.paymentMethod) {
- case 'Stripe':
- case 'Paypal': // FIXME store paypalKey somewhere?
- return b.key == plan.planId;
- default: return undefined;
- }
- })
-
Payments.showStripe = function(data) {
var sub =
data.subscription ? data.subscription
- : data.gift && data.gift.type=='subscription' ? data.gift.subscription.months
+ : data.gift && data.gift.type=='subscription' ? data.gift.subscription.key
: false;
sub = sub && Content.subscriptionBlocks[sub];
var amount = // 500 = $5
@@ -36,7 +25,8 @@ function($rootScope, User, $http, Content) {
token: function(res) {
var url = '/stripe/checkout?a=a'; // just so I can concat &x=x below
if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift);
- if (data.subscription) url += '&sub='+sub.months;
+ if (data.subscription) url += '&sub='+sub.key;
+ if (data.coupon) url += '&coupon='+data.coupon;
$http.post(url, res).success(function() {
window.location.reload(true);
}).error(function(res) {
@@ -50,9 +40,9 @@ function($rootScope, User, $http, Content) {
StripeCheckout.open({
key: window.env.STRIPE_PUB_KEY,
address: false,
- name: 'Update',
- description: 'Update the card to be charged for your subscription',
- panelLabel: 'Update Card',
+ name: window.env.t('subUpdateTitle'),
+ description: window.env.t('subUpdateDescription'),
+ panelLabel: window.env.t('subUpdateCard'),
token: function(data) {
var url = '/stripe/subscribe/edit';
$http.post(url, data).success(function() {
@@ -66,7 +56,7 @@ function($rootScope, User, $http, Content) {
Payments.cancelSubscription = function(){
if (!confirm(window.env.t('sureCancelSub'))) return;
- window.location.href = '/' + plan.paymentMethod.toLowerCase() + '/subscribe/cancel?_id=' + user._id + '&apiToken=' + user.apiToken;
+ window.location.href = '/' + User.user.purchased.plan.paymentMethod.toLowerCase() + '/subscribe/cancel?_id=' + User.user._id + '&apiToken=' + User.user.apiToken;
}
Payments.encodeGift = function(uuid, gift){
diff --git a/src/controllers/auth.js b/src/controllers/auth.js
index 8cf4b9a840..3a00eda7f0 100644
--- a/src/controllers/auth.js
+++ b/src/controllers/auth.js
@@ -148,21 +148,19 @@ api.loginSocial = function(req, res, next) {
network = req.body.network;
if (network!=='facebook')
return res.json(401, {err:"Only Facebook supported currently."});
- async.waterfall([
- function(cb){
+ async.auto({
+ profile: function (cb) {
passport._strategies[network].userProfile(access_token, cb);
},
- function(profile, cb) {
- var q = {};q['auth.'+network+'.id'] = profile.id;
- User.findOne(q, {_id:1, apiToken:1, auth:1}, function(err, user){
- if (err) return cb(err);
- cb(null, {user:user, profile:profile});
- });
- },
- function(data, cb){
- if (data.user) return cb(null, data.user);
+ user: ['profile', function (cb, results) {
+ var q = {};
+ q['auth.' + network + '.id'] = results.profile.id;
+ User.findOne(q, {_id: 1, apiToken: 1, auth: 1}, cb);
+ }],
+ register: ['profile', 'user', function (cb, results) {
+ if (results.user) return cb(null, results.user);
// Create new user
- var prof = data.profile;
+ var prof = results.profile;
var user = {
preferences: {
language: req.language // User language detected from browser, not saved
@@ -175,15 +173,16 @@ api.loginSocial = function(req, res, next) {
user = new User(user);
user.save(cb);
- if(isProd && prof.emails && prof.emails[0] && prof.emails[0].value){
- utils.txnEmail({name:prof.displayName || prof.username, email:prof.emails[0].value}, 'welcome');
+ if (isProd && prof.emails && prof.emails[0] && prof.emails[0].value) {
+ utils.txnEmail({name: prof.displayName || prof.username, email: prof.emails[0].value}, 'welcome');
}
ga.event('register', network).send();
- }
- ], function(err, user){
+ }]
+ }, function(err, results){
if (err) return res.json(401, {err: err.toString ? err.toString() : err});
- if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
- return res.json(200, {id: user.id, token:user.apiToken});
+ var acct = results.register[0] ? results.register[0] : results.register;
+ if (acct.auth.blocked) return res.json(401, accountSuspended(acct._id));
+ return res.json(200, {id:acct._id, token:acct.apiToken});
})
};
@@ -206,16 +205,15 @@ api.resetPassword = function(req, res, next){
if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email});
user.auth.local.salt = salt;
user.auth.local.hashed_password = hashed_password;
- utils.sendEmail({
- from: "HabitRPG ",
- to: email,
- subject: "Password Reset for HabitRPG",
- text: "Password for " + user.auth.local.username + " has been reset to " + newPassword + ". Log in at " + nconf.get('BASE_URL'),
- html: "Password for " + user.auth.local.username + " has been reset to " + newPassword + ". Log in at " + nconf.get('BASE_URL')
+ utils.txnEmail(user, 'reset-password', [
+ {name: "NEW_PASSWORD", content: newPassword},
+ {name: "USERNAME", content: user.auth.local.username}
+ ]);
+ user.save(function(err){
+ if(err) return next(err);
+ res.send('New password sent to '+ email);
+ email = salt = newPassword = hashed_password = null;
});
- user.save();
- res.send('New password sent to '+ email);
- email = salt = newPassword = hashed_password = null;
});
};
diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js
index b5fe4d6982..71e1d82d33 100644
--- a/src/controllers/challenges.js
+++ b/src/controllers/challenges.js
@@ -147,69 +147,63 @@ api.getMember = function(req, res, next) {
// CREATE
api.create = function(req, res, next){
var user = res.locals.user;
- var group, chal;
- // First, make sure they've selected a legit group, and store it for later
- var waterfall = [
- function(cb){
- Group.findById(req.body.group).exec(cb);
+ async.auto({
+ get_group: function(cb){
+ var q = {_id:req.body.group};
+ if (req.body.group!='habitrpg') q.members = {$in:[user._id]}; // make sure they're a member of the group
+ Group.findOne(q, cb);
},
- function(_group, cb){
- if (!_group) return cb("Group." + req.body.group + " not found");
- group = _group;
- cb(null);
- }
- ];
+ save_chal: ['get_group', function(cb, results){
+ var group = results.get_group,
+ prize = +req.body.prize;
+ if (!group)
+ return cb({code:404, err:"Group." + req.body.group + " not found"});
+ if (group.leaderOnly && group.leaderOnly.challenges && group.leader !== user._id)
+ return cb({code:401, err: "Only the group leader can create challenges"});
+ // If they're adding a prize, do some validation
+ if (prize < 0)
+ return cb({code:401, err: 'Challenge prize must be >= 0'});
+ if (req.body.group=='habitrpg' && prize < 1)
+ return cb({code:401, err: 'Prize must be at least 1 Gem for public challenges.'});
+ if (prize > 0) {
+ var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
+ var prizeCost = prize/4; // I really should have stored user.balance as gems rather than dollars... stupid...
+ if (prizeCost > user.balance + groupBalance)
+ return cb("You can't afford this prize. Purchase more gems or lower the prize amount.")
- // If they're adding a prize, do some validation
- if (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'});
- if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'});
- if (+req.body.prize > 0) {
- waterfall.push(function(cb){
- var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
- var prizeCost = req.body.prize/4; // I really should have stored user.balance as gems rather than dollars... stupid...
- if (prizeCost > user.balance + groupBalance)
- return cb("You can't afford this prize. Purchase more gems or lower the prize amount.")
-
- if (groupBalance >= prizeCost) {
- // Group pays for all of prize
- group.balance -= prizeCost;
- } else if (groupBalance > 0) {
- // User pays remainder of prize cost after group
- var remainder = prizeCost - group.balance;
- group.balance = 0;
- user.balance -= remainder;
- } else {
- // User pays for all of prize
- user.balance -= prizeCost;
- }
- cb(null);
- });
- }
-
- waterfall = waterfall.concat([
- function(cb) { // if we're dealing with prize above, arguemnts will be `group, numRows, cb` - else `cb`
+ if (groupBalance >= prizeCost) {
+ // Group pays for all of prize
+ group.balance -= prizeCost;
+ } else if (groupBalance > 0) {
+ // User pays remainder of prize cost after group
+ var remainder = prizeCost - group.balance;
+ group.balance = 0;
+ user.balance -= remainder;
+ } else {
+ // User pays for all of prize
+ user.balance -= prizeCost;
+ }
+ }
req.body.leader = user._id;
req.body.official = user.contributor.admin && req.body.official;
var chal = new Challenge(req.body); // FIXME sanitize
chal.members.push(user._id);
- chal.save(cb)
- },
- function(_chal, num, cb){
- chal = _chal;
- group.challenges.push(chal._id);
- group.save(cb);
- },
- function(_group, num, cb) {
+ chal.save(cb);
+ }],
+ save_group: ['save_chal', function(cb, results){
+ results.get_group.challenges.push(results.save_chal[0]._id);
+ results.get_group.save(cb);
+ }],
+ sync_user: ['save_group', function(cb, results){
// Auto-join creator to challenge (see members.push above)
- chal.syncToUser(user, cb);
- }
- ]);
- async.waterfall(waterfall, function(err){
- if (err) return next(err);
- res.json(chal);
- user = group = chal = null;
- });
+ results.save_chal[0].syncToUser(user, cb);
+ }]
+ }, function(err, results){
+ if (err) return err.code? res.json(err.code, err) : next(err);
+ return res.json(results.save_chal[0]);
+ user = null;
+ })
}
// UPDATE
diff --git a/src/controllers/groups.js b/src/controllers/groups.js
index 50272cdab2..3891c70618 100644
--- a/src/controllers/groups.js
+++ b/src/controllers/groups.js
@@ -7,10 +7,12 @@ function clone(a) {
var _ = require('lodash');
var nconf = require('nconf');
var async = require('async');
+var utils = require('./../utils');
var shared = require('habitrpg-shared');
var User = require('./../models/user').model;
var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model;
+var isProd = nconf.get('NODE_ENV') === 'production';
var api = module.exports;
/*
@@ -187,7 +189,7 @@ api.update = function(req, res, next) {
if(group.leader !== user._id)
return res.json(401, {err: "Only the group leader can update the group!"});
- 'name description logo logo leaderMessage leader'.split(' ').forEach(function(attr){
+ 'name description logo logo leaderMessage leader leaderOnly'.split(' ').forEach(function(attr){
group[attr] = req.body[attr];
});
@@ -256,6 +258,82 @@ api.deleteChatMessage = function(req, res, next){
});
}
+api.flagChatMessage = function(req, res, next){
+ var user = res.locals.user
+ var group = res.locals.group;
+ var message = _.find(group.chat, {id: req.params.mid});
+
+ if(!message) return res.json(404, {err: "Message not found!"});
+ if(message.uuid == user._id) return res.json(401, {err: "Can't report your own message."});
+
+ User.findOne({_id: message.uuid}, {auth: 1}, function(err, author){
+ if(err) return next(err);
+
+ // Log user ids that have flagged the message
+ if(!message.flags) message.flags = {};
+ if(message.flags[user._id] && !user.contributor.admin) return res.json(401, {err: "You have already reported this message"});
+ message.flags[user._id] = true;
+
+ // Log total number of flags (publicly viewable)
+ if(!message.flagCount) message.flagCount = 0;
+ if(user.contributor.admin){
+ // Arbitraty amount, higher than 2
+ message.flagCount = 5;
+ } else {
+ message.flagCount++
+ }
+
+ group.markModified('chat');
+ group.save(function(err,_saved){
+ if(err) return next(err);
+ if (isProd){
+ utils.txnEmail({email: nconf.get('FLAG_REPORT_EMAIL')}, 'flag-report-to-mods', [
+ {name: "MESSAGE_TIME", content: (new Date(message.timestamp)).toString()},
+ {name: "MESSAGE_TEXT", content: message.text},
+
+ {name: "REPORTER_USERNAME", content: user.profile.name},
+ {name: "REPORTER_UUID", content: user._id},
+ {name: "REPORTER_EMAIL", content: user.auth.local ? user.auth.local.email : ((user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0]) ? user.auth.facebook.emails[0].value : null)},
+ {name: "REPORTER_MODAL_URL", content: "https://habitrpg.com/static/front/#?memberId=" + user._id},
+
+ {name: "AUTHOR_USERNAME", content: message.user},
+ {name: "AUTHOR_UUID", content: message.uuid},
+ {name: "AUTHOR_EMAIL", content: author.auth.local ? author.auth.local.email : ((author.auth.facebook && author.auth.facebook.emails && author.auth.facebook.emails[0]) ? author.auth.facebook.emails[0].value : null)},
+ {name: "AUTHOR_MODAL_URL", content: "https://habitrpg.com/static/front/#?memberId=" + message.uuid},
+
+ {name: "GROUP_NAME", content: group.name},
+ {name: "GROUP_TYPE", content: group.type},
+ {name: "GROUP_ID", content: group._id},
+ {name: "GROUP_URL", content: group._id == 'habitrpg' ? (nconf.get('BASE_URL') + '/#/options/groups/tavern') : (group.type === 'guild' ? (nconf.get('BASE_URL')+ '/#/options/groups/guilds/' + group._id) : 'party')},
+ ]);
+ }
+ return res.send(204);
+ });
+ });
+
+}
+
+api.clearFlagCount = function(req, res, next){
+ var user = res.locals.user
+ var group = res.locals.group;
+ var message = _.find(group.chat, {id: req.params.mid});
+
+ if(!message) return res.json(404, {err: "Message not found!"});
+
+ if(user.contributor.admin){
+ message.flagCount = 0;
+
+ group.markModified('chat');
+ group.save(function(err,_saved){
+ if(err) return next(err);
+ return res.send(204);
+ });
+ }else{
+ return res.json(401, {err: "Only an admin can clear the flag count!"})
+ }
+
+}
+
api.seenMessage = function(req,res,next){
// Skip the auth step, we want this to be fast. If !found with uuid/token, then it just doesn't save
// Check for req.params.gid to exist
diff --git a/src/controllers/members.js b/src/controllers/members.js
index 26092e872c..25ffdce25d 100644
--- a/src/controllers/members.js
+++ b/src/controllers/members.js
@@ -35,7 +35,7 @@ api.sendMessage = function(user, member, data){
msg = data.message
} else {
msg = "`Hello " + member.profile.name + ", " + user.profile.name + " has sent you ";
- msg += (data.type=='gems') ? data.gems.amount + " gems!`" : data.subscription.months + " months of subscription!`";
+ msg += (data.type=='gems') ? data.gems.amount + " gems!`" : shared.content.subscriptionBlocks[data.subscription.key].months + " months of subscription!`";
msg += data.message;
}
shared.refPush(member.inbox.messages, groups.chatDefaults(msg, user));
diff --git a/src/controllers/payments/iap.js b/src/controllers/payments/iap.js
new file mode 100644
index 0000000000..29b03fedfb
--- /dev/null
+++ b/src/controllers/payments/iap.js
@@ -0,0 +1,129 @@
+var iap = require('in-app-purchase');
+var async = require('async');
+var payments = require('./index');
+var nconf = require('nconf');
+
+var inAppPurchase = require('in-app-purchase');
+inAppPurchase.config({
+ // this is the path to the directory containing iap-sanbox/iap-live files
+ googlePublicKeyPath: nconf.get("IAP_GOOGLE_KEYDIR")
+});
+
+// Validation ERROR Codes
+var INVALID_PAYLOAD = 6778001;
+var CONNECTION_FAILED = 6778002;
+var PURCHASE_EXPIRED = 6778003;
+
+exports.androidVerify = function(req, res, next) {
+ var iapBody = req.body;
+ var user = res.locals.user;
+
+ iap.setup(function (error) {
+ if (error) {
+ var resObj = {
+ ok: false,
+ data: 'IAP Error'
+ };
+
+ console.error('IAP Setup ERROR');
+ console.error(error);
+
+ res.json(resObj);
+
+ return;
+ }
+
+ /*
+ google receipt must be provided as an object
+ {
+ "data": "{stringified data object}",
+ "signature": "signature from google"
+ }
+ */
+ var testObj = {
+ data: iapBody.transaction.receipt,
+ signature: iapBody.transaction.signature
+ };
+
+ // iap is ready
+ iap.validate(iap.GOOGLE, testObj, function (err, googleRes) {
+ if (err) {
+ var resObj = {
+ ok: false,
+ data: {
+ code: INVALID_PAYLOAD,
+ message: err.toString()
+ }
+ };
+
+ res.json(resObj);
+ console.error(err);
+ return;
+ }
+
+ if (iap.isValidated(googleRes)) {
+ var resObj = {
+ ok: true,
+ data: googleRes
+ };
+
+ payments.buyGems({user:user, paymentMethod:'IAP GooglePlay'});
+
+ // yay good!
+ res.json(resObj);
+ }
+ });
+ });
+};
+
+exports.iosVerify = function(req, res, next) {
+ console.info(req.body);
+
+ var iapBody = req.body;
+ var user = res.locals.user;
+
+ iap.setup(function (error) {
+ if (error) {
+ var resObj = {
+ ok: false,
+ data: 'IAP Error'
+ };
+
+ console.error('IAP Setup ERROR');
+ console.error(error);
+
+ res.json(resObj);
+
+ return;
+ }
+
+ // iap is ready
+ iap.validate(iap.APPLE, iapBody.transaction.receipt, function (err, appleRes) {
+ if (err) {
+ var resObj = {
+ ok: false,
+ data: {
+ code: INVALID_PAYLOAD,
+ message: err.toString()
+ }
+ };
+
+ res.json(resObj);
+ console.error(err);
+ return;
+ }
+
+ if (iap.isValidated(appleRes)) {
+ var resObj = {
+ ok: true,
+ data: appleRes
+ };
+
+ payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
+
+ // yay good!
+ res.json(resObj);
+ }
+ });
+ });
+};
\ No newline at end of file
diff --git a/src/controllers/payments/index.js b/src/controllers/payments/index.js
index 176098b4c5..fea3c73025 100644
--- a/src/controllers/payments/index.js
+++ b/src/controllers/payments/index.js
@@ -7,9 +7,11 @@ var moment = require('moment');
var isProduction = nconf.get("NODE_ENV") === "production";
var stripe = require('./stripe');
var paypal = require('./paypal');
-var User = require('mongoose').model('User');
var members = require('../members')
var async = require('async');
+var iap = require('./iap');
+var mongoose= require('mongoose');
+var cc = require('coupon-code');
function revealMysteryItems(user) {
_.each(shared.content.gear.flat, function(item) {
@@ -29,8 +31,8 @@ exports.createSubscription = function(data, cb) {
var recipient = data.gift ? data.gift.member : data.user;
//if (!recipient.purchased.plan) recipient.purchased.plan = {}; // FIXME double-check, this should never be the case
var p = recipient.purchased.plan;
- var months = +(data.gift ? data.gift.subscription.months : data.sub.months);
- var block = shared.content.subscriptionBlocks[months];
+ var block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
+ var months = +block.months;
if (data.gift) {
if (p.customerId && !p.dateTerminated) { // User has active plan
@@ -114,6 +116,14 @@ exports.buyGems = function(data, cb) {
], cb);
}
+exports.validCoupon = function(req, res, next){
+ mongoose.model('Coupon').findOne({_id:cc.validate(req.params.code), event:'google_6mo'}, function(err, coupon){
+ if (err) return next(err);
+ if (!coupon) return res.json(401, {err:"Invalid coupon code"});
+ return res.send(200);
+ });
+}
+
exports.stripeCheckout = stripe.checkout;
exports.stripeSubscribeCancel = stripe.subscribeCancel;
exports.stripeSubscribeEdit = stripe.subscribeEdit;
@@ -123,4 +133,7 @@ exports.paypalSubscribeSuccess = paypal.executeBillingAgreement;
exports.paypalSubscribeCancel = paypal.cancelSubscription;
exports.paypalCheckout = paypal.createPayment;
exports.paypalCheckoutSuccess = paypal.executePayment;
-exports.paypalIPN = paypal.ipn;
\ No newline at end of file
+exports.paypalIPN = paypal.ipn;
+
+exports.iapAndroidVerify = iap.androidVerify;
+exports.iapIosVerify = iap.iosVerify;
\ No newline at end of file
diff --git a/src/controllers/payments/paypal.js b/src/controllers/payments/paypal.js
index 9c7a1d4f3a..f69ab306da 100644
--- a/src/controllers/payments/paypal.js
+++ b/src/controllers/payments/paypal.js
@@ -9,12 +9,14 @@ var logger = require('../../logging');
var ipn = require('paypal-ipn');
var paypal = require('paypal-rest-sdk');
var shared = require('habitrpg-shared');
+var mongoose = require('mongoose');
+var cc = require('coupon-code');
// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have
// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created
// there, get it's plan.id and store it in config.json
_.each(shared.content.subscriptionBlocks, function(block){
- block.paypalKey = nconf.get("PAYPAL:billing_plans:"+block.months);
+ block.paypalKey = nconf.get("PAYPAL:billing_plans:"+block.key);
});
paypal.configure({
@@ -30,23 +32,33 @@ var parseErr = function(res, err){
}
exports.createBillingAgreement = function(req,res,next){
- req.session.paypalBlock = req.query.sub;
- var block = shared.content.subscriptionBlocks[req.query.sub];
- var billingPlanTitle = "HabitRPG Subscription" + ' ($'+block.price+' every '+block.months+' months, recurring)';
- var billingAgreementAttributes = {
- "name": billingPlanTitle,
- "description": billingPlanTitle,
- "start_date": moment().add({minutes:5}).format(),
- "plan": {
- "id": block.paypalKey
+ var sub = shared.content.subscriptionBlocks[req.query.sub];
+ async.waterfall([
+ function(cb){
+ if (!sub.discount) return cb(null, null);
+ if (!req.query.coupon) return cb('Please provide a coupon code for this plan.');
+ mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb);
},
- "payer": {
- "payment_method": "paypal"
+ function(coupon, cb){
+ if (sub.discount && !coupon) return cb('Invalid coupon code.');
+ var billingPlanTitle = "HabitRPG Subscription" + ' ($'+sub.price+' every '+sub.months+' months, recurring)';
+ var billingAgreementAttributes = {
+ "name": billingPlanTitle,
+ "description": billingPlanTitle,
+ "start_date": moment().add({minutes:5}).format(),
+ "plan": {
+ "id": sub.paypalKey
+ },
+ "payer": {
+ "payment_method": "paypal"
+ }
+ };
+ paypal.billingAgreement.create(billingAgreementAttributes, cb);
}
- };
- paypal.billingAgreement.create(billingAgreementAttributes, function (err, billingAgreement) {
+ ], function(err, billingAgreement){
if (err) return parseErr(res, err);
// For approving subscription via Paypal, first redirect user to: approval_url
+ req.session.paypalBlock = req.query.sub;
var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href;
res.redirect(approval_url);
});
@@ -82,10 +94,10 @@ exports.createPayment = function(req, res) {
var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
var price = !gift ? 5.00
: gift.type=='gems' ? Number(gift.gems.amount/4).toFixed(2)
- : Number(shared.content.subscriptionBlocks[gift.subscription.months].price).toFixed(2);
+ : Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
var description = !gift ? "HabitRPG Gems"
: gift.type=='gems' ? "HabitRPG Gems (Gift)"
- : gift.subscription.months + "mo. HabitRPG Subscription (Gift)";
+ : shared.content.subscriptionBlocks[gift.subscription.key].months + "mo. HabitRPG Subscription (Gift)";
var create_payment = {
"intent": "sale",
"payer": {
diff --git a/src/controllers/payments/paypalBillingSetup.js b/src/controllers/payments/paypalBillingSetup.js
index 38b70c0101..14721e22d1 100644
--- a/src/controllers/payments/paypalBillingSetup.js
+++ b/src/controllers/payments/paypalBillingSetup.js
@@ -11,7 +11,7 @@ var paypal = require('paypal-rest-sdk');
var blocks = require('habitrpg-shared').content.subscriptionBlocks;
var live = nconf.get('PAYPAL:mode')=='live';
-var OP = 'get'; // list create update remove
+var OP = 'create'; // list create update remove
paypal.configure({
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
@@ -72,7 +72,7 @@ switch(OP) {
});
break;
case "create":
- paypal.billingPlan.create(blocks["12"].definition, function(err,plan){
+ paypal.billingPlan.create(blocks["google_6mo"].definition, function(err,plan){
if (err) return console.log(err);
if (plan.state == "ACTIVE")
return console.log({err:err, plan:plan});
diff --git a/src/controllers/payments/stripe.js b/src/controllers/payments/stripe.js
index 117fc29175..74172cc8ab 100644
--- a/src/controllers/payments/stripe.js
+++ b/src/controllers/payments/stripe.js
@@ -4,6 +4,8 @@ var async = require('async');
var payments = require('./index');
var User = require('mongoose').model('User');
var shared = require('habitrpg-shared');
+var mongoose = require('mongoose');
+var cc = require('coupon-code');
/*
Setup Stripe response when posting payment
@@ -17,16 +19,27 @@ exports.checkout = function(req, res, next) {
async.waterfall([
function(cb){
if (sub) {
- stripe.customers.create({
- email: req.body.email,
- metadata: {uuid: user._id},
- card: token,
- plan: sub.key
- }, cb);
+ async.waterfall([
+ function(cb2){
+ if (!sub.discount) return cb2(null, null);
+ if (!req.query.coupon) return cb2('Please provide a coupon code for this plan.');
+ mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb2);
+ },
+ function(coupon, cb2){
+ if (sub.discount && !coupon) return cb2('Invalid coupon code.');
+ var customer = {
+ email: req.body.email,
+ metadata: {uuid: user._id},
+ card: token,
+ plan: sub.key
+ };
+ stripe.customers.create(customer, cb2);
+ }
+ ], cb);
} else {
stripe.charges.create({
amount: !gift ? "500" //"500" = $5
- : gift.type=='subscription' ? ""+shared.content.subscriptionBlocks[gift.subscription.months].price*100
+ : gift.type=='subscription' ? ""+shared.content.subscriptionBlocks[gift.subscription.key].price*100
: ""+gift.gems.amount/4*100,
currency: "usd",
card: token
diff --git a/src/controllers/user.js b/src/controllers/user.js
index bceb3eb677..b4505d95f8 100644
--- a/src/controllers/user.js
+++ b/src/controllers/user.js
@@ -257,37 +257,57 @@ api.update = function(req, res, next) {
};
api.cron = function(req, res, next) {
- var user = res.locals.user,
- progress = user.fns.cron(),
- ranCron = user.isModified(),
- quest = shared.content.quests[user.party.quest.key];
+ try{
+ var user = res.locals.user,
+ progress = user.fns.cron(),
+ ranCron = user.isModified(),
+ quest = shared.content.quests[user.party.quest.key];
+ if (ranCron) res.locals.wasModified = true;
+ if (!ranCron) return next(null,user);
+ Group.tavernBoss(user,progress);
+ if (!quest) return user.save(next);
- if (ranCron) res.locals.wasModified = true;
- if (!ranCron) return next(null,user);
- Group.tavernBoss(user,progress);
- if (!quest) return user.save(next);
+ // FOR DEBUGGING, PLEASE IGNORE
+ var opStatus = null;
- // If user is on a quest, roll for boss & player, or handle collections
- // FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
- async.waterfall([
- function(cb){
- user.save(cb); // make sure to save the cron effects
- },
- function(saved, count, cb){
- var type = quest.boss ? 'boss' : 'collect';
- Group[type+'Quest'](user,progress,cb);
- },
- function(){
- var cb = arguments[arguments.length-1];
- // User has been updated in boss-grapple, reload
- User.findById(user._id, cb);
- }
- ], function(err, saved) {
- res.locals.user = saved;
- next(err,saved);
- user = progress = quest = null;
- });
+ // If user is on a quest, roll for boss & player, or handle collections
+ // FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
+ async.waterfall([
+ function(cb){
+ opStatus = 'saveUser';
+ user.save(cb); // make sure to save the cron effects
+ },
+ function(saved, count, cb){
+ opStatus = 'runQuest';
+ var type = quest.boss ? 'boss' : 'collect';
+ Group[type+'Quest'](user,progress,cb);
+ },
+ function(){
+ var cb = arguments[arguments.length-1];
+ // User has been updated in boss-grapple, reload
+ User.findById(user._id, cb);
+ }
+ ], function(err, saved) {
+ if(err) logging.loggly({
+ error: "Cron caught",
+ stack: (err.stack || err.message || err),
+ body: req.body, headers: req.header,
+ auth: (req.headers['x-api-user'] + ' | ' + req.headers['x-api-key']),
+ originalUrl: req.originalUrl,
+ opStatus: opStatus
+ });
+ res.locals.user = saved;
+ next(err,saved);
+ user = progress = quest = null;
+ });
+ }catch(e){
+ logging.loggly({
+ error: "Cron uncaught",
+ stack: e.stack || e
+ });
+ throw e;
+ }
};
diff --git a/src/logging.js b/src/logging.js
index 5e1449db3d..5c0d973812 100644
--- a/src/logging.js
+++ b/src/logging.js
@@ -3,7 +3,23 @@ var winston = require('winston');
require('winston-mail').Mail;
require('winston-newrelic');
-var logger;
+var logger, loggly;
+
+if (nconf.get('LOGGLY:enabled')){
+ loggly = require('loggly').createClient({
+ token: nconf.get('LOGGLY:token'),
+ subdomain: nconf.get('LOGGLY:subdomain'),
+ auth: {
+ username: nconf.get('LOGGLY:username'),
+ password: nconf.get('LOGGLY:password')
+ },
+ //
+ // Optional: Tag to send with EVERY log message
+ //
+ tags: [('heroku-'+nconf.get('BASE_URL'))],
+ json: true
+ });
+}
if (logger == null) {
logger = new (winston.Logger)({});
@@ -49,3 +65,8 @@ module.exports.error = function(/* variable args */) {
if (logger)
logger.error.apply(logger, arguments);
};
+
+module.exports.loggly = function(/* variable args */){
+ if (loggly)
+ loggly.log.apply(loggly, arguments);
+};
diff --git a/src/middleware.js b/src/middleware.js
index ba5ed01f07..56e72bda7e 100644
--- a/src/middleware.js
+++ b/src/middleware.js
@@ -75,6 +75,13 @@ module.exports.errorHandler = function(err, req, res, next) {
"\n\nbody: " + JSON.stringify(req.body) +
(res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : "");
logging.error(stack);
+ logging.loggly({
+ error: "Uncaught error",
+ stack: (err.stack || err.message || err),
+ body: req.body, headers: req.header,
+ auth: (req.headers['x-api-user'] + ' | ' + req.headers['x-api-key']),
+ originalUrl: req.originalUrl
+ });
var message = err.message ? err.message : err;
message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length);
res.json(500,{err:message}); //res.end(err.message);
@@ -173,12 +180,9 @@ module.exports.locals = function(req, res, next) {
language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined);
var tavern = require('./models/group').tavern;
- res.locals.habitrpg = {
- NODE_ENV: nconf.get('NODE_ENV'),
- BASE_URL: nconf.get('BASE_URL'),
- GA_ID: nconf.get("GA_ID"),
+ var envVars = _.pick(nconf.get(), 'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY'.split(' '));
+ res.locals.habitrpg = _.merge(envVars, {
IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')),
- STRIPE_PUB_KEY: nconf.get('STRIPE_PUB_KEY'),
getManifestFiles: getManifestFiles,
getBuildUrl: getBuildUrl,
avalaibleLanguages: i18n.avalaibleLanguages,
@@ -193,11 +197,9 @@ module.exports.locals = function(req, res, next) {
siteVersion: siteVersion,
Content: shared.content,
mods: require('./models/user').mods,
- FACEBOOK_KEY: nconf.get('FACEBOOK_KEY'),
-
tavern: tavern, // for world boss
worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {}
- };
+ });
// Put query-string party invitations into session to be handled later
try{
diff --git a/src/models/coupon.js b/src/models/coupon.js
index c5122b6ba7..4f2680546c 100644
--- a/src/models/coupon.js
+++ b/src/models/coupon.js
@@ -7,7 +7,7 @@ var autoinc = require('mongoose-id-autoinc');
var CouponSchema = new mongoose.Schema({
_id: {type: String, 'default': cc.generate},
- event: {type:String, enum:['wondercon']},
+ event: {type:String, enum:['wondercon','google_6mo']},
user: {type: 'String', ref: 'User'}
});
@@ -18,38 +18,34 @@ CouponSchema.statics.generate = function(event, count, callback) {
}
CouponSchema.statics.apply = function(user, code, next){
- var _coupon,_user;
- async.waterfall([
- function(cb) {
+ async.auto({
+ get_coupon: function (cb) {
mongoose.model('Coupon').findById(cc.validate(code), cb);
},
- function(coupon, cb) {
- _coupon = coupon;
- if (!coupon) return cb("Invalid coupon code");
- if (coupon.user) return cb("Coupon already used");
- switch (coupon.event) {
+ apply_coupon: ['get_coupon', function (cb, results) {
+ if (!results.get_coupon) return cb("Invalid coupon code");
+ if (results.get_coupon.user) return cb("Coupon already used");
+ switch (results.get_coupon.event) {
case 'wondercon':
- user.items.gear.owned.eyewear_special_wondercon_red = true;
- user.items.gear.owned.eyewear_special_wondercon_black = true;
- user.items.gear.owned.back_special_wondercon_black = true;
- user.items.gear.owned.back_special_wondercon_red = true;
- user.items.gear.owned.body_special_wondercon_red = true;
- user.items.gear.owned.body_special_wondercon_black = true;
- user.items.gear.owned.body_special_wondercon_gold = true;
+ user.items.gear.owned.eyewear_special_wondercon_red = true;
+ user.items.gear.owned.eyewear_special_wondercon_black = true;
+ user.items.gear.owned.back_special_wondercon_black = true;
+ user.items.gear.owned.back_special_wondercon_red = true;
+ user.items.gear.owned.body_special_wondercon_red = true;
+ user.items.gear.owned.body_special_wondercon_black = true;
+ user.items.gear.owned.body_special_wondercon_gold = true;
user.extra = {signupEvent: 'wondercon'};
user.save(cb);
break;
}
- },
- function(user, count, cb){
- _user = user;
- _coupon.user = user._id;
- _coupon.save(cb);
- }
- ], function(err){
+ }],
+ expire_coupon: ['apply_coupon', function (cb, results) {
+ results.get_coupon.user = user._id;
+ results.get_coupon.save(cb);
+ }]
+ }, function(err, results){
if (err) return next(err);
- next(null,_user);
- _coupon = _user = null;
+ next(null,results.apply_coupon[0]);
})
}
diff --git a/src/models/group.js b/src/models/group.js
index 1bc275afe4..2997fd4cfe 100644
--- a/src/models/group.js
+++ b/src/models/group.js
@@ -26,7 +26,10 @@ var GroupSchema = new Schema({
# id: String
# }]
*/
-
+ leaderOnly: { // restrict group actions to leader (members can't do them)
+ challenges: {type:Boolean, 'default':false},
+ //invites: {type:Boolean, 'default':false}
+ },
memberCount: {type: Number, 'default': 0},
challengeCount: {type: Number, 'default': 0},
balance: Number,
@@ -98,7 +101,9 @@ var chatDefaults = module.exports.chatDefaults = function(msg,user){
id: shared.uuid(),
text: msg,
timestamp: +new Date,
- likes: {}
+ likes: {},
+ flags: {},
+ flagCount: 0
};
if (user) {
_.defaults(message, {
@@ -316,6 +321,18 @@ GroupSchema.statics.bossQuest = function(user, progress, cb) {
})
}
+GroupSchema.methods.toJSON = function() {
+ var doc = this.toObject();
+ if(doc.chat){
+ doc.chat.forEach(function(msg){
+ msg.flags = {};
+ });
+ }
+
+ return doc;
+};
+
+
module.exports.schema = GroupSchema;
var Group = module.exports.model = mongoose.model("Group", GroupSchema);
@@ -330,4 +347,4 @@ Group.count({_id:'habitrpg'},function(err,ct){
type: 'guild',
privacy:'public'
}).save();
-})
\ No newline at end of file
+})
diff --git a/src/models/user.js b/src/models/user.js
index f8d3e3795a..3f7b0b3d3e 100644
--- a/src/models/user.js
+++ b/src/models/user.js
@@ -49,7 +49,8 @@ var UserSchema = new Schema({
perfect: Number,
habitBirthday: Boolean,
valentine: Number,
- costumeContest: Boolean
+ costumeContest: Boolean,
+ nye: Number
},
auth: {
blocked: Boolean,
@@ -175,7 +176,9 @@ var UserSchema = new Schema({
snowball: {type: Number, 'default': 0},
spookDust: {type: Number, 'default': 0},
valentine: Number,
- valentineReceived: Array // array of strings, by sender name
+ valentineReceived: Array, // array of strings, by sender name
+ nye: Number,
+ nyeReceived: Array
},
// -------------- Animals -------------------
@@ -280,6 +283,7 @@ var UserSchema = new Schema({
automaticAllocation: Boolean,
allocationMode: {type:String, enum: ['flat','classbased','taskbased'], 'default': 'flat'},
costume: Boolean,
+ dateFormat: {type: String, enum:['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], 'default': 'MM/dd/yyyy'},
sleep: {type: Boolean, 'default': false},
stickyHeader: {type: Boolean, 'default': true},
disableClasses: {type: Boolean, 'default': false},
diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee
index 415577d565..6c93e3ba42 100644
--- a/src/routes/apiv2.coffee
+++ b/src/routes/apiv2.coffee
@@ -577,11 +577,37 @@ module.exports = (swagger, v2) ->
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.likeChatMessage
+ "/groups/{gid}/chat/{mid}/flag":
+ spec:
+ method: 'POST'
+ description: "Flag a chat message"
+ parameters: [
+ path 'gid','Group id','string'
+ path 'mid','Message id','string'
+ ]
+ middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
+ action: groups.flagChatMessage
+
+ "/groups/{gid}/chat/{mid}/clearflags":
+ spec:
+ method: 'POST'
+ description: "Clear flag count from message and unhide it"
+ parameters: [
+ path 'gid','Group id','string'
+ path 'mid','Message id','string'
+ ]
+ middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
+ action: groups.clearFlagCount
+
# ---------------------------------
# Members
# ---------------------------------
- "/members/{uuid}":
- spec:{}
+ "/members/{uuid}:GET":
+ spec:
+ path: '/members/{uuid}'
+ description: "Get a member."
+ parameters: [path('uuid','Member ID','string')]
+ middleware: [auth.auth, i18n.getUserLanguage]
action: members.getMember
"/members/{uuid}/message":
spec:
@@ -589,7 +615,7 @@ module.exports = (swagger, v2) ->
description: 'Send a private message to a member'
parameters: [
path 'uuid', 'The UUID of the member to message', 'string'
- body '', '{message: "The private message to send"}', 'object'
+ body '', '{"message": "The private message to send"}', 'object'
]
middleware: [auth.auth]
action: members.sendPrivateMessage
@@ -608,7 +634,7 @@ module.exports = (swagger, v2) ->
description: 'Send a gift to a member'
parameters: [
path 'uuid', 'The UUID of the member', 'string'
- body '', '{gems:{amount:Number, fromBalance:Boolean}, subscription:{months:Number}}', 'object'
+ body '', '{"type": "gems or subscription", "gems":{"amount":Number, "fromBalance":Boolean}, "subscription":{"months":Number}}', 'object'
]
middleware: [auth.auth]
action: members.sendGift
diff --git a/src/routes/payments.js b/src/routes/payments.js
index c34d77b24b..118f923689 100644
--- a/src/routes/payments.js
+++ b/src/routes/payments.js
@@ -17,4 +17,9 @@ router.post("/stripe/subscribe/edit", auth.auth, i18n.getUserLanguage, payments.
//router.get("/stripe/subscribe", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribe); // checkout route is used (above) with ?plan= instead
router.get("/stripe/subscribe/cancel", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribeCancel);
+router.post("/iap/android/verify", auth.authWithUrl, /*i18n.getUserLanguage, */payments.iapAndroidVerify);
+router.post("/iap/ios/verify", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.iapIosVerify);
+
+router.get("/api/v2/coupons/valid-discount/:code", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.validCoupon);
+
module.exports = router;
\ No newline at end of file
diff --git a/src/utils.js b/src/utils.js
index ba8dd47f95..f37d7cd9d0 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -35,6 +35,8 @@ function getMailingInfo(user) {
}
module.exports.txnEmail = function(mailingInfo, emailType, variables){
+ var variables = [{name: 'BASE_URL', content: nconf.get('BASE_URL')}].concat(variables || []);
+
if (mailingInfo._id) mailingInfo = getMailingInfo(mailingInfo);
if (!mailingInfo.email) return;
request({
diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee
index 28b2a97c3d..70521f91a1 100644
--- a/test/api.mocha.coffee
+++ b/test/api.mocha.coffee
@@ -141,7 +141,7 @@ describe "API", ->
id: res.body.todos[0].id
body:
- dateCompleted: moment().subtract("days", 4)
+ dateCompleted: moment().subtract(4, "days")
}
{
op: "updateTask"
@@ -149,7 +149,7 @@ describe "API", ->
id: res.body.todos[1].id
body:
- dateCompleted: moment().subtract("days", 4)
+ dateCompleted: moment().subtract(4, "days")
}
]).end (res) ->
expect(_.size(res.body.todos)).to.be 4
@@ -460,7 +460,7 @@ describe "API", ->
{
op: "update"
body:
- lastCron: moment().subtract("days", 1)
+ lastCron: moment().subtract(1, "days")
}
]).end (res) ->
expect(res.body.party.quest.progress.up).to.be.above up
@@ -532,7 +532,7 @@ describe "API", ->
{
op: "update"
body:
- lastCron: moment().subtract("days", 1)
+ lastCron: moment().subtract(1, "days")
}
]).end ->
cb()
@@ -608,7 +608,7 @@ describe "API", ->
it "Handles unsubscription", (done) ->
cron = ->
- user.lastCron = moment().subtract("d", 1)
+ user.lastCron = moment().subtract(1, "d")
user.fns.cron()
expect(user.purchased.plan.customerId).to.not.be.ok()
@@ -624,7 +624,7 @@ describe "API", ->
cron()
expect(user.purchased.plan.customerId).to.be.ok()
expect(user.purchased.plan.dateTerminated).to.be.ok()
- user.purchased.plan.dateTerminated = moment().subtract("d", 2)
+ user.purchased.plan.dateTerminated = moment().subtract(2, "d")
cron()
expect(user.purchased.plan.customerId).to.not.be.ok()
payments.createSubscription user,
diff --git a/views/options/inventory/index.jade b/views/options/inventory/index.jade
index af191f3de1..63959b1708 100644
--- a/views/options/inventory/index.jade
+++ b/views/options/inventory/index.jade
@@ -15,9 +15,12 @@ script(type='text/ng-template', id='partials/options.inventory.html')
li.equipment-tab(ng-class="{ active: $state.includes('options.inventory.equipment') }")
a(ui-sref='options.inventory.equipment')
=env.t('equipment')
- li.equipment-tab(ng-class="{ active: $state.includes('options.inventory.timetravelers') }")
+ li(ng-class="{ active: $state.includes('options.inventory.timetravelers') }")
a(ui-sref='options.inventory.timetravelers')
- | Time Travelers
+ =env.t('timeTravelers')
+ li(ng-class="{ active: $state.includes('options.inventory.seasonalshop') }")
+ a(ui-sref='options.inventory.seasonalshop')
+ =env.t('seasonalShop')
.tab-content
.tab-pane.active
diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade
index 3d872231a6..27ff9a6c52 100644
--- a/views/options/inventory/inventory.jade
+++ b/views/options/inventory/inventory.jade
@@ -19,6 +19,42 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html')
div(ng-repeat='item in gear[klass]')
button.customize-option(popover='{{::item.notes()}}', popover-title='{{::item.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{type:"costume", key:item.key}})', class='shop_{{::item.key}}', ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}')
+script(type='text/ng-template', id='partials/options.inventory.seasonalshop.html')
+ .container-fluid
+ .stable.row
+ .col-md-2
+ .seasonalshop_winter2015
+ .col-md-10
+ .popover.static-popover.fade.right.in
+ .arrow
+ h3.popover-title!=env.t('seasonalShopTitle', {linkStart:"", linkEnd: ""})
+ .popover-content
+ p!=env.t('seasonalShopText')
+ li.customize-menu.inventory-gear
+ menu.pets-menu(label='{{::label}}', ng-repeat='(set,label) in ::{candycane:env.t("candycaneSet"), ski:env.t("skiSet"), snowflake:env.t("snowflakeSet"), yeti:env.t("yetiSet")}')
+ div(ng-repeat='item in ::getSeasonalShopArray(set)' ng-class="{transparent: user.items.gear.owned[item.key]}")
+ button.customize-option(popover='{{::item.notes()}}', popover-title='{{::item.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='purchase(item.type,item)', class='shop_{{::item.key}}')
+ .text-center
+ | {{((item.specialClass == "wizard") && (item.type == "weapon")) + 1}}
+ span.Pet_Currency_Gem1x.inline-gems
+ menu.pets-menu(label=env.t('quests'))
+ div(ng-repeat='quest in ::getSeasonalShopQuests()')
+ button.customize-option(popover="{{::quest.previous && !user.achievements.quests[quest.previous] ? env.t('scrollsPre') : quest.notes() | htmlDecode}}", popover-title='{{::quest.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='buyQuest(quest.key)', class='inventory_quest_scroll', ng-class='::{locked: quest.previous && !user.achievements.quests[quest.previous]}')
+ p
+ | {{::quest.value}}
+ span.Pet_Currency_Gem1x.inline-gems
+ menu.pets-menu(label=env.t('seasonalItems'))
+ div
+ button.customize-option(popover='{{::Content.spells.special.snowball.notes()}}', popover-title='{{::Content.spells.special.snowball.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='purchase("special", Content.spells.special.snowball)', class='inventory_special_snowball')
+ p
+ | {{::Content.spells.special.snowball.value}}
+ span(class='shop_gold')
+ div
+ button.customize-option(popover='{{::Content.spells.special.nye.notes()}}', popover-title='{{::Content.spells.special.nye.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='castStart(Content.spells.special.nye)', class='inventory_special_nye')
+ p
+ | {{Content.spells.special.nye.value}}
+ span(class='shop_gold')
+
script(type='text/ng-template', id='partials/options.inventory.timetravelers.html')
.container-fluid
.stable.row(ng-if='user.purchased.plan.consecutive.trinkets <= 0')
@@ -83,7 +119,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
li.customize-menu
menu.pets-menu(label=env.t('food') + ' ({{foodCount}})')
- p(ng-show='foodCount < 1')=env.t('noFood')
+ p.muted(ng-show='foodCount < 1')=env.t('noFood')
div(ng-repeat='(food,points) in ownedItems(user.items.food)')
button.customize-option(popover='{{::Content.food[food].notes()}}', popover-title='{{::Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseFood(food)', class='Pet_Food_{{::food}}')
.badge.badge-info.stack-count {{points}}
@@ -98,12 +134,10 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
+specialItem('snowball')
+specialItem('spookDust')
- //-menu.pets-menu(label=env.t('valentineCard'))
div(ng-if='user.items.special.valentineReceived[0]')
button.customize-option(popover="Valentine's Day Card from {{User.user.items.special.valentineReceived[0]}}", popover-trigger='mouseenter', popover-placement='right', ng-click='openModal("valentine")', class='inventory_special_valentine')
.badge.badge-info.stack-count {{user.items.special.valentineReceived.length}}
- //-menu.pets-menu(label=env.t('subscriberItem'))
div(ng-if='user.purchased.plan.customerId || user.purchased.plan.mysteryItems.length')
button.customize-option(popover=env.t('subscriberItemText'), popover-trigger='mouseenter', popover-placement='right', class='inventory_present', ng-click="user.ops.openMysteryItem({})")
.badge.badge-info.stack-count {{user.purchased.plan.mysteryItems.length}}
@@ -112,6 +146,10 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
button.customize-option(popover=env.t('mysticHourglassPopover'), popover-trigger='mouseenter', popover-placement='right', class='inventory_special_trinket', ng-click="$state.go('options.inventory.timetravelers')")
.badge.badge-info.stack-count {{user.purchased.plan.consecutive.trinkets}}
+ div(ng-if='user.items.special.nyeReceived[0]')
+ button.customize-option(popover="New Year's Card from {{User.user.items.special.nyeReceived[0]}}", popover-trigger='mouseenter', popover-placement='right', ng-click='openModal("nye")', class='inventory_special_nye')
+ .badge.badge-info.stack-count {{user.items.special.nyeReceived.length}}
+
.col-md-6
h2=env.t('market')
.row-fluid
@@ -178,15 +216,6 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
p
| 4
span.Pet_Currency_Gem1x.inline-gems
- mixin buySpecialSpell(k,gp)
- div
- button.customize-option(popover='{{::Content.spells.special.#{k}.notes()}}', popover-title='{{::Content.spells.special.#{k}.text()}}', popover-trigger='mouseenter', popover-placement='top', ng-click='purchase("special", Content.spells.special.#{k})', class='inventory_special_#{k}')
- p
- | {{::Content.spells.special.#{k}.value}}
- span(class='#{gp ? "shop_gold" : "Pet_Currency_Gem1x inline-gems"}')
- //-+buySpecialSpell('snowball',false)
- //-+buySpecialSpell('spookDust',true)
-
div(ng-show='user.flags.rebirthEnabled')
button.customize-option(popover=env.t('rebirthPop'), popover-title=env.t('rebirthName'), popover-trigger='mouseenter', popover-placement='top', ng-click='openModal("rebirth")', class='rebirth_orb')
p(ng-show='user.stats.lvl < 100')
@@ -197,7 +226,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
p
| 4
span.Pet_Currency_Gem1x.inline-gems
- div(ng-if='user.purchased.plan.customerId')
+ div(ng-if='user.purchased.plan.customerId', ng-class='::{transparent:(Shared.planGemLimits.convCap + User.user.purchased.plan.consecutive.gemCapExtra - User.user.purchased.plan.gemsBought) < 1}')
button.customize-option(popover=env.t('subGemPop'), popover-title=env.t('subGemName'), popover-trigger='mouseenter', popover-placement='top',ng-click='user.ops.purchase({params:{type:"gems",key:"gem"}})')
span.Pet_Currency_Gem.inline-gems
.badge.badge-success.stack-count {{Shared.planGemLimits.convCap + User.user.purchased.plan.consecutive.gemCapExtra - User.user.purchased.plan.gemsBought}}
diff --git a/views/options/profile.jade b/views/options/profile.jade
index 6e212061dd..715b32072f 100644
--- a/views/options/profile.jade
+++ b/views/options/profile.jade
@@ -15,8 +15,9 @@ mixin customizeProfile(mobile)
if ~['limited','seasonal'].indexOf(status)
.label.label-info.pull-right.hint(popover=limited, popover-title=env.t(status+'Edition'), popover-placement='right', popover-trigger='mouseenter')=env.t(status+'Edition')
menu(label=env.t(title))
- +gemCost(2)
- button.btn.btn-xs(ng-hide='#{status=="disabled"} || #{showPath("user.purchased."+path, colors, "||")}', ng-click='#{unlockPath(path, colors)}')!= env.t('unlockSet',{cost:5}) + ' '
+ span(ng-hide='#{status=="disabled"} || #{showPath("user.purchased."+path, colors, "&&")}')
+ +gemCost(2)
+ button.btn.btn-xs(ng-click='#{unlockPath(path, colors)}')!= env.t('unlockSet',{cost:5}) + ' '
each color in colors
button.customize-option(type='button', class='#{path=="skin" ? "skin_"+color : "customize-option hair hair_bangs_1_"+color}', ng-class='{locked: !user.purchased.#{path}["#{color}"]}', ng-if='#{status!="disabled"} || user.purchased.#{path}["#{color}"]', ng-click='unlock("#{path}.#{color}")')
@@ -38,8 +39,9 @@ mixin customizeProfile(mobile)
menu(label=env.t('specialShirts'))
- var specialShirts = ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie']
- +gemCost(2)
- button.btn.btn-xs(ng-hide='#{showPath("user.purchased.shirt", specialShirts, "&&")}', ng-click='#{unlockPath("shirt",specialShirts)}')!= env.t('unlockSet',{cost:5}) + ' '
+ span(ng-hide='#{showPath("user.purchased.shirt", specialShirts, "&&")}')
+ +gemCost(2)
+ button.btn.btn-xs(ng-click='#{unlockPath("shirt",specialShirts)}')!= env.t('unlockSet',{cost:5}) + ' '
each shirt in specialShirts
button.customize-option(type='button', class='{{user.preferences.size}}_shirt_'+shirt, ng-class='{locked: !user.purchased.shirt.'+shirt+'}', ng-click='unlock("shirt.'+shirt+'")')
@@ -61,6 +63,7 @@ mixin customizeProfile(mobile)
button(type='button', ng-if='user.purchased.hair.color.#{color}', class='customize-option hair hair_bangs_1_#{color}', ng-click='unlock("hair.color.#{color}")')
+buyPref('hair.color', ['rainbow','yellow','green','purple','blue','TRUred'], 'rainbowColors')
+buyPref('hair.color', ['candycorn','ghostwhite','halloween','midnight','pumpkin','zombie'], 'hauntedColors', 'disabled')
+ +buyPref('hair.color', ['aurora','festive','hollygreen','peppermint','snowy','winterstar'], 'winteryColors')
li.customize-menu
menu(label=env.t('bodyHair'))
@@ -80,8 +83,9 @@ mixin customizeProfile(mobile)
// Purchasable hairstyles
menu(label=env.t('hairSet1'))
- var colors = [2,4,5,6,7,8]
- +gemCost(2)
- button.btn.btn-xs(ng-hide='#{showPath("user.purchased.hair", colors, "&&")}', ng-click='#{unlockPath("hair.base",colors)}')!= env.t('unlockSet',{cost:5}) + ' '
+ span(ng-hide='#{showPath("user.purchased.hair.base", colors, "&&")}')
+ +gemCost(2)
+ button.btn.btn-xs(ng-click='#{unlockPath("hair.base",colors)}')!= env.t('unlockSet',{cost:5}) + ' '
each num in colors
button(class='hair_base_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base["#{num}"]}', ng-click='unlock("hair.base.#{num}")')
@@ -93,9 +97,10 @@ mixin customizeProfile(mobile)
button(class='hair_flower_#{num} customize-option', type='button', ng-click='set({"preferences.hair.flower":#{num}})')
li.customize-menu
- h5=env.t('bodyFacialHair')
- +gemCost(2)
- button.btn.btn-xs(ng-hide='user.purchased.hair.mustache["1"] && user.purchased.hair.mustache["2"] && user.purchased.hair.beard["1"] && user.purchased.hair.beard["2"] && user.purchased.hair.beard["3"]', ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockSet',{cost:5}) + ' '
+ menu(label=env.t('bodyFacialHair'))
+ span(ng-hide='user.purchased.hair.mustache["1"] && user.purchased.hair.mustache["2"] && user.purchased.hair.beard["1"] && user.purchased.hair.beard["2"] && user.purchased.hair.beard["3"]')
+ +gemCost(2)
+ button.btn.btn-xs(ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockSet',{cost:5}) + ' '
// Beard
menu(label=env.t('beard'))
@@ -249,9 +254,9 @@ mixin backgrounds(mobile)
- var k = bgsKeys[i], bgs = env.Content.backgrounds[k];
li.customize-menu
menu(label=env.t(k))
- +gemCost(7)
- //-button.btn.btn-xs(ng-hide="ownsSet('background',Content.backgrounds['#{k}'])",ng-click="unlock(setKeys('background',Content.backgrounds['#{k}']))")!= env.t('unlockSet',{cost:15}) + ' '
- button.btn.btn-xs(ng-hide="ownsSet('background',#{JSON.stringify(bgs)})",ng-click="unlock(setKeys('background',#{JSON.stringify(bgs)}))")!= env.t('unlockSet',{cost:15}) + ' '
+ span(ng-hide="ownsSet('background',#{JSON.stringify(bgs)})")
+ +gemCost(7)
+ button.btn.btn-xs(ng-click="unlock(setKeys('background',#{JSON.stringify(bgs)}))")!= env.t('unlockSet',{cost:15}) + ' '
each bg,k in bgs
button.customize-option(type='button', class='background_#{k}', ng-class="user.purchased.background.#{k} ? 'background-unlocked' : 'background-locked'", ng-click='unlock("background.#{k}")', popover-title=bg.text(), popover=bg.notes(),popover-trigger='mouseenter')
i.glyphicon.glyphicon-lock(ng-if="!user.purchased.background.#{k}")
diff --git a/views/options/settings.jade b/views/options/settings.jade
index ef2727b4f6..3ba5192ec8 100644
--- a/views/options/settings.jade
+++ b/views/options/settings.jade
@@ -36,6 +36,10 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
br
strong
!=env.t('helpWithTranslation')
+
+ .form-horizontal
+ h5=env.t('dateFormat')
+ select.form-control(ng-model='user.preferences.dateFormat', ng-options='DF for DF in availableFormats', ng-change='set({"preferences.dateFormat": user.preferences.dateFormat})')
.checkbox
label
input(type='checkbox', ng-click='hideHeader() ', ng-checked='user.preferences.hideHeader!==true')
@@ -200,7 +204,7 @@ mixin subPerks()
tr
td
span.hint(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right') #{env.t('buyGemsGold')}
- span.badge.badge-success(ng-show='_subscription.months>1') Cap raised to {{ [25 + user.purchased.plan.consecutive.gemCapExtra + Math.floor(_subscription.months/3*5), 50] | min }}
+ span.badge.badge-success(ng-show='_subscription.key!="basic_earned"') Cap raised to {{ [25 + user.purchased.plan.consecutive.gemCapExtra + Math.floor(Content.subscriptionBlocks[_subscription.key].months/3*5), 50] | min }}
tr
td
span.hint(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory')
@@ -210,8 +214,8 @@ mixin subPerks()
tr
td
span.hint(popover=env.t('mysteryItemText'),popover-trigger='mouseenter',popover-placement='right') #{env.t('mysteryItem')}
- div(ng-show='_subscription.months>1')
- .badge.badge-success +{{Math.floor(_subscription.months/3)}} Mystic Hourglass
+ div(ng-show='_subscription.key!="basic_earned"')
+ .badge.badge-success +{{Math.floor(Content.subscriptionBlocks[_subscription.key].months/3)}} Mystic Hourglass
.small.muted Mystic Hourglasses allow purchasing a previous month's Mystery Item set.
tr
td
@@ -224,7 +228,7 @@ script(id='partials/feature-matrix-check.html',type='text/ng-template')
script(id='partials/options.settings.subscription.html',type='text/ng-template')
//-h2=env.t('individualSub')
- .container-fluid(ng-init='_subscription={months:1}')
+ .container-fluid(ng-init='_subscription={key:"basic_earned"}')
.row
.col-md-6
h3 Benefits
@@ -237,7 +241,7 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
| #{env.t('subCanceled')} {{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}
tr(ng-if='!user.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
- p(ng-if='Payments.currentSub') Recurring ${{Payments.currentSub.price}} each {{Payments.currentSub.months}} Month(s) ({{user.purchased.plan.paymentMethod}})
+ p(ng-if='user.purchased.plan.planId') Recurring ${{Content.subscriptionBlocks[user.purchased.plan.planId].price}} each {{Content.subscriptionBlocks[user.purchased.plan.planId].months}} Month(s) ({{user.purchased.plan.paymentMethod}})
tr(ng-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| You have {{user.purchased.plan.extraMonths | number:2}} months of subscription credit.
@@ -250,16 +254,23 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
li Mystic Hourglasses: {{user.purchased.plan.consecutive.trinkets}}
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
.form-group
- each block in env.Content.subscriptionBlocks
- .radio
- label
- input(type="radio", name="subRadio", value="#{block.months}", ng-model='_subscription.months')
- //-| #{block.months} Month(s) Recurring: $#{block.price} #{env.t('monthUSD')}
- | Recurring $#{block.price} each #{block.months} Month(s)
+ .radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit:"discount==true" | orderBy:"months"')
+ label
+ input(type="radio", name="subRadio", ng-value="block.key", ng-model='_subscription.key')
+ span(ng-show='block.original')
+ | Recurring ${{block.original}} ${{block.price}} each {{block.months}} Month(s)
+ span(ng-hide='block.original')
+ | Recurring ${{block.price}} each {{block.months}} Month(s)
+
+ .form-inline
+ .form-group
+ input.form-control(type='text', ng-model='_subscription.coupon', placeholder='Promo Code')
+ .form-group
+ button.btn.btn-small(type='button',ng-click='applyCoupon(_subscription.coupon)') Apply
h3(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)') Resubscribe
- a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.months})', ng-disabled='!_subscription.months') Card
- a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.months}}', ng-disabled='!_subscription.months') PayPal
+ a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key') Card
+ a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key') PayPal
div(ng-if='user.purchased.plan.customerId')
- .btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()') Update Card
+ .btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')
\ No newline at end of file
diff --git a/views/options/social/chat-message.jade b/views/options/social/chat-message.jade
index 120dc2de42..34ff439004 100644
--- a/views/options/social/chat-message.jade
+++ b/views/options/social/chat-message.jade
@@ -1,17 +1,20 @@
mixin chatMessages(inbox)
ul.list-unstyled.tavern-chat
- var ngRepeat = inbox ? 'message in user.inbox.messages | toArray:true | orderBy:"sort":true' : 'message in group.chat track by message.id'
- li.chat-message(ng-repeat=ngRepeat, ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}', style='{{::message.sent ? "opacity:0.5" : ""}}')
- .scrollable-message
+ li.chat-message(ng-repeat=ngRepeat, ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}', ng-if="!message.flagCount || message.flagCount < 2 || user.contributor.admin")
+ span.pull-right.text-danger(ng-if="user.contributor.admin && message.flagCount > 0")
+ | {{message.flagCount > 1 ? "Message Hidden" : "1 flag"}}
+ .scrollable-message(ng-class='{"transparent": message.sent || message.flags[user._id] || (user.contributor.admin && message.flagCount > 1)}')
span(ng-if='::message.user')
a.label.label-default.chat-message.hidden-label
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
+ |
span {{::message.user}}
span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)')
// this invisible username label is here to push the message text far enough right that the visible label can be floated to this point without covering up any of the text
markdown(ng-model='::message.text')
| -
- span.muted.time(from-now='::message.timestamp' tooltip="{{::message.timestamp | date:'short'}}")
+ span.muted.time(from-now='::message.timestamp' tooltip="{{::message.timestamp | date:user.preferences.dateFormat.concat(' HH:mm:ss')}}")
unless inbox
span
a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}}
@@ -22,11 +25,15 @@ mixin chatMessages(inbox)
if inbox
a(ng-click="quickReply(message.uuid)")
span.glyphicon.glyphicon-share-alt(tooltip=env.t('pm-reply'))
- |
+ |
a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}')
span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
- span.float-label
+ |
+ a(ng-click="flagChatMessage(group._id, message)", ng-if=':: user.contributor.admin || (!message.sent && user.flags.communityGuidelinesAccepted && message.uuid != user.id && message.uuid != "system")')
+ span.glyphicon.glyphicon-flag(tooltip="{{message.flags[user._id] ? env.t('abuseAlreadyReported') : env.t('abuseFlag')}}" ng-class='message.flags[user._id] ? "text-danger" : ""')
+ span.float-label(ng-class='::contribText(message.contributor, message.backer).length > 30 ? "long-title" : ""')
a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)')
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
+ |
span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}}
span(ng-class='::userAdminGlyphiconStyleFromLevel(message.contributor.level)')
diff --git a/views/options/social/create-group.jade b/views/options/social/create-group.jade
index b2211862c8..6c0c4c20e8 100644
--- a/views/options/social/create-group.jade
+++ b/views/options/social/create-group.jade
@@ -19,5 +19,10 @@ form.col-md-12.form-horizontal(ng-submit='create(newGroup)')
span.gem-cost= '4 ' + env.t('gems')
p
small=env.t('gemCost')
+ .form-group
+ .checkbox
+ label
+ input(type='checkbox', ng-model='newGroup.leaderOnly.challenges')
+ | Only group leader can create challenges
.form-group(ng-show='type=="party"')
input.btn.btn-default.form-control(type='submit', value=env.t('create'))
diff --git a/views/options/social/group.jade b/views/options/social/group.jade
index 1b1d7a6f84..a3c5e257d8 100644
--- a/views/options/social/group.jade
+++ b/views/options/social/group.jade
@@ -33,6 +33,11 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
.form-group
label=env.t('logoUrl')
input.form-control(type='url', placeholder=env.t('logoUrl'), ng-model='group.logo')
+ .form-group
+ .checkbox
+ label
+ input(type='checkbox', ng-model='group.leaderOnly.challenges')
+ | Only group leader can create challenges
h4=env.t('assignLeader')
select#group-leader-selection(ng-model='group._newLeader', ng-options='member.profile.name for member in group.members')
diff --git a/views/options/social/index.jade b/views/options/social/index.jade
index 27e66c0eba..24b86ab7e5 100644
--- a/views/options/social/index.jade
+++ b/views/options/social/index.jade
@@ -16,6 +16,7 @@ script(type='text/ng-template', id='partials/options.social.inbox.html')
.checkbox
label
input(type='checkbox', ng-model='user.inbox.optOut', ng-change='set({"inbox.optOut": user.inbox.optOut?true: false})')
+ |
span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('optOutPopover'))=env.t('optOut')
script(type='text/ng-template', id='partials/options.social.tavern.html')
@@ -104,13 +105,13 @@ script(type='text/ng-template', id='partials/options.social.html')
a(ui-sref='options.social.party')
=env.t('party')
li(ng-class="{ active: $state.includes('options.social.guilds') }")
- a(ui-sref='options.social.guilds')
+ a(ui-sref='options.social.guilds.public')
=env.t('guilds')
li(ng-class="{ active: $state.includes('options.social.challenges') }")
a(ui-sref='options.social.challenges')
=env.t('challenges')
li(ng-class="{ active: $state.includes('options.social.hall') }")
- a(ui-sref='options.social.hall')
+ a(ui-sref='options.social.hall.heroes')
=env.t('hall')
.tab-content
diff --git a/views/options/social/tavern.jade b/views/options/social/tavern.jade
index 9eea1159f3..0739126588 100644
--- a/views/options/social/tavern.jade
+++ b/views/options/social/tavern.jade
@@ -21,7 +21,7 @@
span(ng-hide='user.preferences.sleep')=env.t('innCheckIn')
=env.t('danielText2')
.alert.alert-info(ng-show='user.preferences.sleep')
- =env.t('innText')
+ =env.t('innText',{name:"{{user.profile.name}}"})
// Resources
.panel.panel-default
diff --git a/views/shared/header/menu.jade b/views/shared/header/menu.jade
index 486cdd95f3..467fbeeab7 100644
--- a/views/shared/header/menu.jade
+++ b/views/shared/header/menu.jade
@@ -34,11 +34,11 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.social.party')=env.t('party')
li
- a(ui-sref='options.social.guilds')=env.t('guilds')
+ a(ui-sref='options.social.guilds.public')=env.t('guilds')
li
a(ui-sref='options.social.challenges')=env.t('challenges')
li
- a(ui-sref='options.social.hall')=env.t('hall')
+ a(ui-sref='options.social.hall.heroes')=env.t('hall')
ul.toolbar-submenu
li
a(ui-sref='options.inventory.drops')=env.t('market')
@@ -49,7 +49,9 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.inventory.equipment')=env.t('equipment')
li
- a(ui-sref='options.inventory.timetravelers') Time Travelers
+ a(ui-sref='options.inventory.timetravelers')=env.t('timeTravelers')
+ li
+ a(ui-sref='options.inventory.seasonalshop')=env.t('seasonalShop')
ul.toolbar-submenu
li
a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool')
@@ -110,11 +112,11 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.social.party')=env.t('party')
li
- a(ui-sref='options.social.guilds')=env.t('guilds')
+ a(ui-sref='options.social.guilds.public')=env.t('guilds')
li
a(ui-sref='options.social.challenges')=env.t('challenges')
li
- a(ui-sref='options.social.hall')=env.t('hall')
+ a(ui-sref='options.social.hall.heroes')=env.t('hall')
li.toolbar-button-dropdown
a(ui-sref='options.inventory.drops')
span=env.t('inventory')
@@ -131,7 +133,9 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.inventory.equipment')=env.t('equipment')
li
- a(ui-sref='options.inventory.timetravelers') Time Travelers
+ a(ui-sref='options.inventory.timetravelers')=env.t('timeTravelers')
+ li
+ a(ui-sref='options.inventory.seasonalshop')=env.t('seasonalShop')
li.toolbar-button-dropdown
a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')
span=env.t('data')
diff --git a/views/shared/modals/limited.jade b/views/shared/modals/limited.jade
index 8d2d31ce90..bdf7049fa9 100644
--- a/views/shared/modals/limited.jade
+++ b/views/shared/modals/limited.jade
@@ -27,4 +27,30 @@ script(id='modals/valentine.html', type='text/ng-template')
p
small For enduring such a saccharine poem, you both receive the "Adoring Friends" badge!
.modal-footer
- button.btn.btn-default(ng-click='user.ops.readValentine({});$close()')=env.t('ok')
\ No newline at end of file
+ button.btn.btn-default(ng-click='user.ops.readValentine({});$close()')=env.t('ok')
+
+// New Year's
+script(id='modals/nye.html', type='text/ng-template')
+ .modal-header
+ h4
+ .inventory_special_nye.pull-right
+ =env.t('nyeCard')
+ .modal-body
+ .bg-info(style='padding:10px')
+ p To: {{user.profile.name}}, From: {{user.items.special.nyeReceived[0]}}
+ hr
+ ul.list-unstyled(ng-switch='::Math.floor(Math.random()*5)')
+ li(ng-switch-when='0')
+ =env.t('newYear0')
+ li(ng-switch-when='1')
+ =env.t('newYear1')
+ li(ng-switch-when='2')
+ =env.t('newYear2')
+ li(ng-switch-when='3')
+ =env.t('newYear3')
+ li(ng-switch-default)
+ =env.t('newYear4')
+ p
+ small For celebrating the new year together, you both receive the "Auld Acquaintance" badge!
+ .modal-footer
+ button.btn.btn-default(ng-click='user.ops.readNYE({});$close()')=env.t('ok')
diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade
index 595929c819..fbe64c6279 100644
--- a/views/shared/modals/members.jade
+++ b/views/shared/modals/members.jade
@@ -15,12 +15,12 @@ script(type='text/ng-template', id='modals/member.html')
|
=env.t('memberSince')
|
- | {{::timestamp(profile.auth.timestamps.created)}} -
+ | {{::profile.auth.timestamps.created | date:user.preferences.dateFormat}} -
li(ng-show='::profile.auth.timestamps.loggedin')
|
=env.t('lastLoggedIn')
|
- | {{::timestamp(profile.auth.timestamps.loggedin)}} -
+ | {{::profile.auth.timestamps.loggedin | date:user.preferences.dateFormat}} -
h3=env.t('stats')
.label.label-info {{:: {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }}
include ../profiles/stats
@@ -46,7 +46,7 @@ script(type='text/ng-template', id='modals/private-message.html')
.modal-header
h4=env.t('pmHeading', {name: "{{profile.profile.name}}"})
.modal-body
- textarea.form-control(type='text',ng-model='_message')
+ textarea.form-control(type='text',rows='5',ng-model='_message')
.modal-footer
button.btn.btn-primary(ng-click='sendPrivateMessage(profile._id, _message)')=env.t("send")
button.btn.btn-default(ng-click='$close()')=env.t('cancel')
@@ -75,11 +75,10 @@ script(type='text/ng-template', id='modals/send-gift.html')
.panel-heading Subscription
.panel-body
.form-group
- each block in env.Content.subscriptionBlocks
- .radio
- label
- input(type="radio", name="subRadio", value="#{block.months}", ng-model='gift.subscription.months')
- | #{block.months} Month(s): $#{block.price}
+ .radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit:"discount==true" | orderBy:"months"')
+ label
+ input(type="radio", name="subRadio", ng-value="block.key", ng-model='gift.subscription.key')
+ | {{block.months}} Month(s): ${{block.price}}
textarea.form-control(rows='3', ng-model='gift.message', placeholder='Personal message (optional)')
@@ -88,4 +87,17 @@ script(type='text/ng-template', id='modals/send-gift.html')
button.btn.btn-primary(ng-show=fromBal, ng-click='sendGift(profile._id, gift)')=env.t("send")
a.btn.btn-primary(ng-hide=fromBal, ng-click='Payments.showStripe({gift:gift, uuid:profile._id})')=env.t('card')
a.btn.btn-warning(ng-hide=fromBal, href='/paypal/checkout?_id={{::user._id}}&apiToken={{::user.apiToken}}&gift={{Payments.encodeGift(profile._id, gift)}}') PayPal
- button.btn.btn-default(ng-click='$close()')=env.t('cancel')
\ No newline at end of file
+ button.btn.btn-default(ng-click='$close()')=env.t('cancel')
+
+script(type='text/ng-template', id='modals/abuse-flag.html')
+ .modal-header
+ h4!=env.t('abuseFlagModalHeading', {name: "{{profile.profile.name}}"})
+ .modal-body
+ blockquote
+ markdown(ng-model="abuseObject.text")
+ p!=env.t('abuseFlagModalBody', {firstLinkStart: "", secondLinkStart: "", linkEnd: ""})
+ .modal-footer
+ button.pull-left.btn.btn-danger(ng-click='clearFlagCount(abuseObject, groupId)', ng-if='user.contributor.admin && abuseObject.flagCount >= 2')
+ | Reset Flag Count
+ button.btn.btn-primary(ng-click='$close()')=env.t('cancel')
+ button.btn.btn-default(ng-click='reportAbuse(user, abuseObject, groupId)')=env.t("abuseFlagModalButton")
diff --git a/views/shared/modals/quests.jade b/views/shared/modals/quests.jade
index ccd4d39850..6ccf0dd06f 100644
--- a/views/shared/modals/quests.jade
+++ b/views/shared/modals/quests.jade
@@ -5,6 +5,7 @@ script(type='text/ng-template', id='modals/questCompleted.html')
h4 "{{::Content.quests[user.party.quest.completed].text()}}"
=env.t('completed')
.modal-body
+ div(ng-class='::Content.quests[user.party.quest.completed].completion() ? "pull-right-sm" : ""', class='col-centered quest_{{user.party.quest.completed}}')
p(ng-bind-html='::Content.quests[user.party.quest.completed].completion()')
quest-rewards(key='{{user.party.quest.completed}}', header=env.t('youReceived'))
.modal-footer
diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade
index b059de69df..ad4c8fca75 100644
--- a/views/shared/new-stuff.jade
+++ b/views/shared/new-stuff.jade
@@ -1,19 +1,114 @@
-h5 PENGUIN PET QUEST AND WINTER PLOT-LINE
+h5 NEW YEAR'S EVE CELEBRATION: Party Hats, New Year's Cards, Snowballs, and Last Chance for Penguin Item Set
tr
td
- h5 Penguin Pet Quest
- .quest_penguin.pull-right
- p Habiticans wanted to go ice-skating, but instead, a giant penguin is freezing everything in sight! All we wanted was to go ice-skating... Can you get this penguin to chill out? If so, you'll be rewarded with some penguins of your own!
- p.small.muted by Melynnrose, Breadstrings, Rattify, Painter de Cluster, Daniel the Bard, and Leephon
+ h5 Party Hats
+ .promo_partyhats.pull-right
+ p In honor of the new year, some free Party Hats are available in the Rewards store! New users get the ever-handsome Absurd Party Hat, and users who already received one last year get the Silly Party Hat. These hats will be available to purchase until January 31st, but once you've bought them, you'll have them forever. Enjoy!
+ p.small.muted by Lemoness and SabreCat
tr
td
- h5 Winter Plot-Line
- p Lemoness enters the Tavern with worrying news from the far north of Habitica. "Nobody's heard from the Stoïkalm Steppes for over a week," she says. "It's hard to imagine anything troubling the citizens there, since it's such a placid part of the continent... But just in case, maybe I should pay a visit." Sounds like a good plan to us!
+ h5 New Year's Cards (Until Jan 1st Only!)
+ .inventory_special_nye.pull-right
+ p Until January 1st only, the Seasonal Shop is stocking New Year's Cards! Now you can send cards to your friends (and yourself) to wish them a Happy Habit New Year. All senders and recipients will receive the Auld Acquaintance badge! When you receive a card, it will appear in your Inventory. Click it to receive a seasonal message!
+ p.small.muted by Lemoness and SabreCat
+ tr
+ td
+ h5 Snowballs
+ .inventory_special_snowball.pull-right
+ p The Seasonal Shop is also stocking Snowballs for gold! Throw them at your friends to have an exciting effect. Anyone hit with a snowball earns the Annoying Friends badge. The results of being hit with a Snowball will last until the end of your day, but you can also reverse them early by buying Salt from the Rewards column. Snowballs are available until January 31st.
+ p.small.muted by Shaner, Lemoness, and SabreCat
+ tr
+ td
+ h5 Last Chance for Penguin Item Set
+ p Reminder: this is the final day to subscribe and receive the Penguin Item Set! If you want the Penguin Hat or the Penguin Suit, now's the time! Thanks so much for your support <3
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
+ h5 12/25/2014 - December Subscriber Item Set and Seasonal Shop! Plus, Flagging Posts and Winter Plot-Line Continues
+ tr
+ td
+ h5 December Subscriber Item Set
+ .promo_mystery_201412.pull-right
+ p The December Subscriber Item has been revealed: the Penguin Item Set! All December subscribers will receive the Penguin Hat and the Penguin Suit. You still have six days to subscribe) and receive the item set! Thank you so much for your support - we really do rely on you to keep HabitRPG free to use and running smoothly.
+ p.small.muted by Lemoness
+ tr
+ td
+ h5 Seasonal Shop: Seasonal Outfits and Quests
+ .seasonalshop_winter2015.pull-right
+ p The Seasonal Shop has opened! The Seasonal Sorceress is stocking the seasonal edition versions of last year's winter outfits, now available for Gems instead of Gold, and the two winter quests, Trapper Santa and Find the Cub. The Seasonal Shop will only be open until January 31st, so don't wait!
+ p.small.muted by SabreCat and Lemoness
+ tr
+ td
+ h5 Flagging Posts
+ p You can now report inappropriate posts to moderators simply by clicking the new flag button next to the post. You should only report posts that violate the Community Guidelines and/or Terms of Service. Thanks for helping us to keep Habitica safe and pleasant for everybody!
+ p.small.muted by Alys, Blade, and Matteo
+ tr
+ td
+ h5 Winter Plot-Line Continues
+ p SabreCat's news is dire. "Most of my sabertooth friends have been impossible to reach, but one thing is clear: the prides have been disappearing from the Steppes. There are also reports that something drove the mammoths to early migration and disturbed the hibernation of the terrible lizards."
+ p He wraps his cloak around himself as another blast of frigid wind roars through the streets. An icy winter gale has been blowing from the north, rattling the window panes and setting the pets and mounts to trembling and howling.
+ p "I've never seen anything like it!" says Matt the Beast Master. "Something is terrifying all my animals - even the cacti, who are normally so mighty and brave! For something to frighten a cactus..." He shakes his head.
+ p The stress level in Habitica is mounting.
+ p.small.muted Missed the previous Winter Plot-line? Catch up on the story here!
+ p.small.muted by Lemoness
+ h5 12/21/2014 - Winter Wonderland Begins, Winter Class Outfits, Wintery Hair Colors, and NPC Decorations!
+ tr
+ td
+ h5 Winter Wonderland Begins!
+ p Winter has arrived, and the snow is gently drifting down over Habit City. Come celebrate with us!
+ tr
+ td
+ h5 Winter Class Outfits
+ .promo_winterclasses2015.pull-right
+ p From now until January 31st, limited edition outfits are available in the Rewards column. Depending on your class, you can be a Soothing Skater, Mage of the North, Gingerbread Warrior, or Icicle Drake! You'd better get productive to earn enough gold before they disappear. Good luck!
+ p.small.muted by Lemoness
+ tr
+ td
+ h5 Wintery Hair Colors
+ .promo_winteryhair.pull-right
+ p The Seasonal Edition Wintery Hair Colors are now available for purchase in the avatar customizations page! Now you can dye your avatar's hair Holly Green, Winter Star, Snowy, Peppermint, Aurora, or Festive.
+ p Seasonal Edition items recur unchanged every year, but they are only available to purchase during a short period of time. This is different from Limited Edition Items, which only recur if something is changed, such as the art or the price. These hair colors may remind some of you of the Holiday Hair Colors that were available last winter. The Holiday Hair Colors have been Retired in favor of the similar Seasonal Edition Wintery Hair Colors. Read more about the difference between Seasonal and Limited Edition items here!
+ p.small.muted by Lemoness, crystalphoenix, and mariahm
+ tr
+ td
+ h5 NPC Decorations
+ .npc_alex.pull-right
+ p Looks like the NPCs are really getting in to the cheery winter mood around the site. Who wouldn't? After all, there's plenty more to come!
+ p.small.muted by Lemoness
+ h5 12/17/2014 - Android App Update, Seasonal Shop, and Winter Plot-Line Continues
+ tr
+ td
+ h5 Android App Update: December Art and Buying Gems!
+ p The December backgrounds and penguin pet quest are now visible in the Android mobile app! Also, we’ve made it possible to buy gems directly from the app. Now you don’t have to switch to the website to stock up!
+ p You can get the Android app here! We will announce when the iOS app is available as well.
+ p.small.muted by negue
+ tr
+ td
+ h5 Seasonal Shop Tab
+ .seasonalshop_closed.pull-right
+ p Looks like a new tab has appeared under Inventory - the Seasonal Shop! It's still closed, but I've heard a rumor that it will open soon...
+ p.small.muted by SabreCat and Lemoness
+ tr
+ td
+ h5 Winter Plot-Line Continues
+ p Lemoness bursts into the Tavern, shaking icicles off her hat. "The Stoïkalm Steppes are completely abandoned!" she says, gulping the cup of tea that Daniel the Barkeep offers her. "No people milling about, no mounts and pets playing in the snow - and when I tried to fly closer, my dragon spooked and refused to land!"
+ p A cloaked figure in the corner steps into the fire light - SabreCat, a powerful adventurer from the north. "The Stoïkalm Steppes are the last home of many animals that have long since gone extinct elsewhere," he says. "The stoic Stoïkalmers would never flee their lands unless something was threatening their pets and mounts!"
+ p He turns to Lemoness. "I can speak the language of the northern beasts. I'll try to contact the roaming sabertooth prides to see if they know what happened." As he lopes off into the distance, a cold wind begins to blow.
+ p.muted Missed the first part of the Winter Plot-Line? Read it here.
+ h5 12/9/2014 - Penguin Pet Quest and Winter Plot-Line
+ tr
+ td
+ h5 Penguin Pet Quest
+ .quest_penguin.pull-right
+ p Habiticans wanted to go ice-skating, but instead, a giant penguin is freezing everything in sight! All we wanted was to go ice-skating... Can you get this penguin to chill out? If so, you'll be rewarded with some penguins of your own!
+ p.small.muted by Melynnrose, Breadstrings, Rattify, Painter de Cluster, Daniel the Bard, and Leephon
+ tr
+ td
+ h5 Winter Plot-Line
+ p Lemoness enters the Tavern with worrying news from the far north of Habitica. "Nobody's heard from the Stoïkalm Steppes for over a week," she says. "It's hard to imagine anything troubling the citizens there, since it's such a placid part of the continent... But just in case, maybe I should pay a visit." Sounds like a good plan to us!
+
h5 12/3/2014 - Gifting Subscriptions And Gems, New Subscription Benefits, Mysterious Time Travelers, Steampunk Item Sets, And Block Subscriptions!
table.table.table-striped
tr
diff --git a/views/shared/profiles/achievements.jade b/views/shared/profiles/achievements.jade
index ac61999d44..88702029a4 100644
--- a/views/shared/profiles/achievements.jade
+++ b/views/shared/profiles/achievements.jade
@@ -187,3 +187,9 @@ div(ng-if='::profile.achievements.costumeContest')
=env.t('costumeContestText')
hr
+div(ng-if='::profile.achievements.nye')
+ .achievement.achievement-nye
+ h5=env.t('auldAcquaintance')
+ small
+ =env.t('auldAcquaintanceText', {cards: "{{::profile.achievements.nye}}"})
+ hr
diff --git a/views/shared/tasks/lists.jade b/views/shared/tasks/lists.jade
index 0cc9abe767..372410dda7 100644
--- a/views/shared/tasks/lists.jade
+++ b/views/shared/tasks/lists.jade
@@ -103,14 +103,14 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
span.task-notes
span.glyphicon.glyphicon-comment
//left-hand size commands
- .task-controls
+ .task-controls.task-primary
a.money.btn-buy.item-btn(ng-click='castStart(Content.spells.special.#{k})')
if canceler
- span.reward-cost {{Content.spells.special.#{k}.value}}
span.shop_gold
+ span.reward-cost {{Content.spells.special.#{k}.value}}
else
+ span.shop_spell(class='shop_#{k}')
span.reward-cost {{user.items.special.#{k}}}
- span(class='shop_#{k}',style='display:inline-block;vertical-align:top;')
// main content
p.task-text {{Content.spells.special.#{k}.text()}}
diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade
index 0466ff91e1..e3cf0b5cac 100644
--- a/views/shared/tasks/task.jade
+++ b/views/shared/tasks/task.jade
@@ -4,7 +4,8 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
// Due Date
span(ng-if='task.type=="todo" && task.date')
- span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{moment(task.date).format('MM/DD')}}
+ span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:user.preferences.dateFormat.substr(0,5)}}
+
// Streak
|
span(ng-show='task.streak') {{task.streak}}
@@ -82,7 +83,7 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
//-| {{task.text}}
div(ng-if='task.checklist && !$state.includes("options.social.challenges") && !task.collapseChecklist && !task._editing')
- fieldset.option-group
+ fieldset.option-group.task-checklist
label.checkbox(ng-repeat='item in task.checklist')
input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)')
markdown(ng-model='item.text',target='_blank')
@@ -122,25 +123,24 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
a(ng-click="unlink(task, 'remove-all')")=env.t('removeThem')
// Checklists
- div(ng-if='!$state.includes("options.social.challenges")')
- button.btn.btn-default.btn-sm(ng-if='!task.checklist[0] && (task.type=="daily" || task.type=="todo")',ng-click='addChecklist(task)')
- span.glyphicon.glyphicon-tasks
- |
- =env.t('addChecklist')
+ .task-checklist-edit(ng-if='!$state.includes("options.social.challenges")')
+ ul
+ li
+ button(type='button', ng-if='!task.checklist[0] && (task.type=="daily" || task.type=="todo")',ng-click='addChecklist(task)')
+ span.glyphicon.glyphicon-tasks
+ span=env.t('addChecklist')
form.checklist-form(ng-if='task.checklist')
- fieldset.option-group.task-checklist(ng-if='!$state.includes("options.social.challenges")')
+ fieldset.option-group(ng-if='!$state.includes("options.social.challenges")')
legend.option-title
span.hint(popover=env.t('checklistText'),popover-trigger='mouseenter',popover-placement='bottom')=env.t('checklist')
- ul(hrpg-sort-checklist).list-unstyled
+ ul(hrpg-sort-checklist)
li(ng-repeat='item in task.checklist')
- a.pull-right.checklist-icons(ng-click='removeChecklistItem(task,$event,$index,true)')
- span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
- span.checklist-icons.glyphicon.glyphicon-resize-vertical()
- |
- input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)')
+ //input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)')
//-,ng-blur='saveTask(task,true)')
- input.inline-edit(type='text',ng-model='item.text',ui-keydown="{'8 46':'removeChecklistItem(task,$event,$index)'}",ui-keyup="{'13':'addChecklistItem(task,$event,$index)','38 40':'navigateChecklist(task,$index,$event)'}")
-
+ span.checklist-icon.glyphicon.glyphicon-resize-vertical()
+ input(type='text',ng-model='item.text',ui-keydown="{'8 46':'removeChecklistItem(task,$event,$index)'}",ui-keyup="{'13':'addChecklistItem(task,$event,$index)','38 40':'navigateChecklist(task,$index,$event)'}")
+ a(ng-click='removeChecklistItem(task,$event,$index,true)')
+ span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
form(ng-submit='saveTask(task,false,true)')
// text & notes
@@ -152,27 +152,34 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
textarea.option-content(rows='3', ng-model='task.notes', ng-model-options="{debounce: 1000}")
// if Habit, plus/minus command options
- fieldset.option-group(ng-if='task.type=="habit" && !task.challenge.id')
+ fieldset.option-group.plusminus(ng-if='task.type=="habit" && !task.challenge.id')
legend.option-title=env.t('direction/Actions')
- span.task-checker.action-plusminus.select-toggle
+ span.task-checker
input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-plus', type='checkbox', ng-model='task.up')
label(for='{{obj._id}}_{{task.id}}-option-plus')
- span.task-checker.action-plusminus.select-toggle
+ span.task-checker
input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-minus', type='checkbox', ng-model='task.down')
label(for='{{obj._id}}_{{task.id}}-option-minus')
// if Daily, calendar
fieldset(ng-if='::task.type=="daily"', class="option-group")
legend.option-title=env.t('repeat')
- .task-controls.tile-group.repeat-days(bindonce)
+ ul.repeat-days(bindonce)
// note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding
- button.task-action-btn.tile(ng-class='{active: task.repeat.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)') {{::moment.weekdaysMin(0)}}
- button.task-action-btn.tile(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)') {{::moment.weekdaysMin(1)}}
- button.task-action-btn.tile(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)') {{::moment.weekdaysMin(2)}}
- button.task-action-btn.tile(ng-class='{active: task.repeat.w}', type='button', ng-click='task.challenge.id || (task.repeat.w = !task.repeat.w)') {{::moment.weekdaysMin(3)}}
- button.task-action-btn.tile(ng-class='{active: task.repeat.th}', type='button', ng-click='task.challenge.id || (task.repeat.th = !task.repeat.th)') {{::moment.weekdaysMin(4)}}
- button.task-action-btn.tile(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}}
- button.task-action-btn.tile(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}}
+ li
+ button(ng-class='{active: task.repeat.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)') {{::moment.weekdaysMin(0)}}
+ li
+ button(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)') {{::moment.weekdaysMin(1)}}
+ li
+ button(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)') {{::moment.weekdaysMin(2)}}
+ li
+ button(ng-class='{active: task.repeat.w}', type='button', ng-click='task.challenge.id || (task.repeat.w = !task.repeat.w)') {{::moment.weekdaysMin(3)}}
+ li
+ button(ng-class='{active: task.repeat.th}', type='button', ng-click='task.challenge.id || (task.repeat.th = !task.repeat.th)') {{::moment.weekdaysMin(4)}}
+ li
+ button(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}}
+ li
+ button(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}}
// if Reward, pricing
fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id')
@@ -184,7 +191,7 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
// if Todos, the due date
fieldset.option-group(ng-if='task.type=="todo" && !task.challenge.id')
legend.option-title=env.t('dueDate')
- input.option-content.datepicker(type='text', datepicker-popup='MM/dd/yyyy', ng-model='task.date', is-open='datepickerOpened', ng-click='datepickerOpened = true')
+ input.option-content.datepicker(type='text', datepicker-popup='{{user.preferences.dateFormat}}', ng-model='task.date', is-open='datepickerOpened', ng-click='datepickerOpened = true')
// Tags
fieldset.option-group(ng-if='!$state.includes("options.social.challenges")')
@@ -199,10 +206,13 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
fieldset.option-group.advanced-option(ng-class="{visuallyhidden: task._advanced}")
legend.option-title
a.hint.priority-multiplier-help(href='https://trello.com/card/priority-multiplier/50e5d3684fe3a7266b0036d6/17', target='_blank', popover-title=env.t('difficultyHelpTitle'), popover-trigger='mouseenter', popover=env.t('difficultyHelpContent'))=env.t('difficulty')
- .task-controls.tile-group.priority-multiplier
- button.task-action-btn.tile(type='button', ng-class='{active: task.priority==1 || !task.priority}', ng-click='task.challenge.id || (task.priority=1)')=env.t('easy')
- button.task-action-btn.tile(type='button', ng-class='{active: task.priority==1.5}', ng-click='task.challenge.id || (task.priority=1.5)')=env.t('medium')
- button.task-action-btn.tile(type='button', ng-class='{active: task.priority==2}', ng-click='task.challenge.id || (task.priority=2)')=env.t('hard')
+ ul.priority-multiplier
+ li
+ button(type='button', ng-class='{active: task.priority==1 || !task.priority}', ng-click='task.challenge.id || (task.priority=1)')=env.t('easy')
+ li
+ button(type='button', ng-class='{active: task.priority==1.5}', ng-click='task.challenge.id || (task.priority=1.5)')=env.t('medium')
+ li
+ button(type='button', ng-class='{active: task.priority==2}', ng-click='task.challenge.id || (task.priority=2)')=env.t('hard')
//span(ng-if='task.type=="daily" && !task.challenge.id')
span(ng-if='task.type=="daily"')
legend.option-title.pull-left=env.t('restoreStreak')
@@ -210,12 +220,17 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
div(ng-if='(user.preferences.allocationMode == "taskbased" && user.preferences.automaticAllocation) || $state.is("options.social.challenges")')
legend.option-title.pull-left=env.t('attributes')
- .task-controls.tile-group.task-attributes
- button.task-action-btn.tile(type='button', ng-class='{active: task.attribute=="str"}', ng-click='task.attribute="str"')=env.t('physical')
- button.task-action-btn.tile(type='button', ng-class='{active: task.attribute=="int"}', ng-click='task.attribute="int"')=env.t('mental')
- button.task-action-btn.tile(type='button', ng-class='{active: task.attribute=="con"}', ng-click='task.attribute="con"')=env.t('social')
- button.task-action-btn.tile(type='button', ng-class='{active: task.attribute=="per"}', ng-click='task.attribute="per"', popover=env.t('otherExamples'), popover-trigger='mouseenter', popover-placement='top')=env.t('other')
+ ul.task-attributes
+ li
+ button(type='button', ng-class='{active: task.attribute=="str"}', ng-click='task.attribute="str"')=env.t('physical')
+ li
+ button(type='button', ng-class='{active: task.attribute=="int"}', ng-click='task.attribute="int"')=env.t('mental')
+ li
+ button(type='button', ng-class='{active: task.attribute=="con"}', ng-click='task.attribute="con"')=env.t('social')
+ li
+ button(type='button', ng-class='{active: task.attribute=="per"}', ng-click='task.attribute="per"', popover=env.t('otherExamples'), popover-trigger='mouseenter', popover-placement='top')=env.t('other')
- button.task-action-btn.tile.spacious(type='submit')=env.t('saveAndClose')
+ .save-close
+ button(type='submit')=env.t('saveAndClose')
div(class='{{obj._id}}{{task.id}}-chart', ng-show='charts[obj._id+task.id]')