Compare commits

...

34 Commits

Author SHA1 Message Date
Matteo Pagliazzi
97e1d75dce 3.60.0 2016-12-12 22:04:34 +01:00
Matteo Pagliazzi
52bf20c27d upgrade shrinkwrap 2016-12-12 22:01:39 +01:00
Matteo Pagliazzi
5dbaf39aba Node 6 and NPM 4 (#8081)
* upgrade node to version 6

* upgrade npm to v4

* update shrinkwrap

* use npm 4 in travis

* use mongoose 4.6.4

* update shrinkwrap

* fix async test and upgrade mongoose

* fix amazon test

* remove debugging code

* working tests with separate server

* update coupon code

* mupdate mongoose

* nvm: relax node version in .nvmrc
2016-12-12 21:51:53 +01:00
Sabe Jones
66d402c985 3.59.1 2016-12-12 20:33:12 +00:00
Sabe Jones
8048146223 chore(news): Bailey 2016-12-12 20:17:58 +00:00
Matteo Pagliazzi
e2c07e458d client: fix action name 2016-12-12 21:05:51 +01:00
padm0
90a9e8e192 Fixing 112016 mystery set. (#8276) 2016-12-12 09:13:59 -08:00
Keith Holliday
f8039f48a6 Styled merch button to have highlight (#8273) 2016-12-10 22:09:06 -06:00
PowerlinxJetfire
04337f8e83 Fix filter buttons when windows resizes fixes (partially) #7772 (#8258)
* Reloads the quest panel solving issue #7697

* Revert "Reloads the quest panel solving issue #7697"

This reverts commit 0d58fb0fd3.

* fix overlapping filter buttons when windows resizes

This fixes one of the two causes of issue #7772.
https://github.com/HabitRPG/habitica/issues/7772
2016-12-10 22:08:26 -06:00
Keith Holliday
45297f8bf9 Merge pull request #8256 from a2lin/equipment_search
Adds a free-text filter (search) to the equipment page.
2016-12-10 16:28:11 -06:00
Keith Holliday
6f112c29f2 Merge pull request #8268 from Tallestthomas/develop
Show link leading to the Food Preferences Wiki page.
2016-12-10 14:41:17 -06:00
Keith Holliday
4d1edb363c Merge pull request #8243 from 15313-platypi/develop
Quest Panel Reload (Issue #7697)
2016-12-10 14:34:43 -06:00
Alexander Lin
4e303cc592 Clean up code 2016-12-10 02:15:30 -08:00
Travis
798a975185 fix: confirm no user objects reference a group before deleting it when the member count reaches 0 (#8267)
* fix: confirm no user objects reference a group before deleting it when the member count reaches 0

* Updating mongo queries to return promises and use the select statement.
2016-12-09 12:14:25 -08:00
Keith Holliday
eb2b46fc5d Merge pull request #8269 from Hus274/8265
Adding a merchandise link to the marketplace selector
2016-12-09 11:57:36 -08:00
Keith Holliday
29854d3bdb Merge pull request #8271 from Hus274/8266
Adding habitica mugs to the static merchandise page
2016-12-09 11:54:45 -08:00
Sabe Jones
f8751b002c fix(subs): record creation for gifts 2016-12-09 19:52:17 +00:00
Travis
cd545e08d5 Implementing retries on failed user updates when finishing a quest (#8251)
* Implementing retries on failed user updates when finishing a quest. fixes #8035

* Refactoring mongo db retries to use the same as code path as original call and moving retries to count based over time based.

* Adding tests for retry logic and updating retries to happen recursively

* Moving callbacks to promises and other tweaks according to pr.

* Chaging mongoose promise to use .catch() functionality

* If all retries fail, the system will now throw an error instead of returning an error message.
2016-12-09 11:30:17 -08:00
Travis Husman
f69bb4f023 Adding habitica mugs to the static/merch page. fixes #8266 2016-12-09 10:35:00 -08:00
Tom Rasmussen
847081d2b2 Removed extra newlines 2016-12-09 12:01:05 -05:00
Travis Husman
8112d46ea4 Removing line break 2016-12-09 07:32:40 -08:00
Travis Husman
d13bded647 Updating the merchandise link to look like a list item. 2016-12-09 07:31:48 -08:00
Matteo Pagliazzi
1de4ab3612 client: namespaces for actions and getteters 2016-12-08 23:01:59 -08:00
Keith Holliday
f9f22f313f Merge pull request #8261 from b9chris/develop
Fix a bug where 1005-768 the avatars and health bar get covered up.
2016-12-08 18:50:18 -08:00
tallestthomas
cbef83c14a Removed yarn.lock and added it to the .gitignore 2016-12-08 21:31:14 -05:00
Travis Husman
605a5a1d5c Adding a merchandise link to the marketplace selector. fixes #8265 2016-12-08 08:19:13 -08:00
tallestthomas
2d5d786c8e Added pet food link to pets.json and jade template (issue #8023) 2016-12-08 11:05:22 -05:00
Alexander Lin
3e92bb22fa Refactor, add spec tests for equipment search 2016-12-08 00:08:18 -08:00
tallestthomas
79829ca128 Added link to pet food wiki (#8023) 2016-12-07 10:30:29 -05:00
Chris Moschini
adaa1d9a3e Fix a bug where 1005-768 the avatars and health bar get covered up. 2016-12-07 08:55:36 -05:00
Alexander Lin
679459b83b Adds free-text filter to equipment page
Closes #8241
2016-12-05 01:06:56 -08:00
Marcel Oyuela-Bonzani
ad76ab1315 Triple Equals sign 2016-12-04 13:40:46 -05:00
Marcel Oyuela-Bonzani
15eb8db925 Fix for issue noted. 2016-12-04 04:31:04 -05:00
Marcel Oyuela-Bonzani
0d58fb0fd3 Reloads the quest panel solving issue #7697 2016-11-28 22:29:20 -05:00
50 changed files with 1364 additions and 901 deletions

View File

@@ -14,7 +14,7 @@ files:
owner: root
group: users
content: |
$(ls -td /opt/elasticbeanstalk/node-install/node-* | head -1)/bin/npm install -g npm@3
$(ls -td /opt/elasticbeanstalk/node-install/node-* | head -1)/bin/npm install -g npm@4
container_commands:
01_makeBabel:
command: "touch /tmp/.babel.json"

2
.nvmrc
View File

@@ -1 +1 @@
4.3.1
6

View File

@@ -1,8 +1,8 @@
language: node_js
node_js:
- '4.3.1'
- '6'
before_install:
- npm install -g npm@3
- npm install -g npm@4
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
before_script:
- npm run test:build

View File

@@ -17,7 +17,7 @@ RUN apt-get install -y \
python
# Install NodeJS
RUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
RUN apt-get install -y nodejs
# Clean up package management
@@ -25,7 +25,7 @@ RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# Install global packages
RUN npm install -g npm@3
RUN npm install -g npm@4
RUN npm install -g gulp grunt-cli bower
# Clone Habitica repo and install dependencies

View File

@@ -318,7 +318,7 @@ gulp.task('test:api-v3:integration:watch', () => {
gulp.task('test:api-v3:integration:separate-server', (done) => {
let runner = exec(
testBin('mocha test/api/v3/integration --recursive', 'LOAD_SERVER=0'),
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'),
{maxBuffer: 500 * 1024},
(err, stdout, stderr) => done(err)
);

659
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.59.0",
"version": "3.60.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "3.6.0",
@@ -15,8 +15,10 @@
"aws-sdk": "^2.0.25",
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-transform-async-to-module-method": "^6.8.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-regenerator": "^6.16.1",
"babel-polyfill": "^6.6.1",
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
@@ -29,7 +31,7 @@
"compression": "^1.6.1",
"connect-ratelimit": "0.0.7",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.3",
"coupon-code": "^0.4.5",
"css-loader": "^0.23.1",
"csv-stringify": "^1.0.2",
"cwait": "^1.0.0",
@@ -75,7 +77,7 @@
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.13.0",
"mongoose": "4.6.6",
"mongoose": "^4.7.1",
"mongoose-id-autoinc": "~2013.7.14-4",
"morgan": "^1.7.0",
"nconf": "~0.8.2",
@@ -126,8 +128,8 @@
},
"private": true,
"engines": {
"node": "^4.3.1",
"npm": "^3.8.9"
"node": "^6.9.1",
"npm": "^4.0.2"
},
"scripts": {
"lint": "eslint --ext .js,.vue .",

View File

@@ -10,13 +10,11 @@ describe('payments - amazon - #createOrderReferenceId', () => {
user = await generateUser();
});
it('verifies billingAgreementId', async (done) => {
try {
await user.post(endpoint);
} catch (e) {
// Parameter AWSAccessKeyId cannot be empty.
expect(e.error).to.eql('BadRequest');
done();
}
it('verifies billingAgreementId', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Missing req.body.billingAgreementId',
});
});
});

View File

@@ -145,6 +145,14 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.dateUpdated).to.exist;
});
it('sets plan.dateCreated if it did not previously exist', async () => {
expect(recipient.purchased.plan.dateCreated).to.not.exist;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCreated).to.exist;
});
it('does not change plan.customerId if it already exists', async () => {
recipient.purchased.plan = plan;
data.customerId = 'purchaserCustomerId';

View File

@@ -628,24 +628,89 @@ describe('Group Model', () => {
});
});
it('deletes a private group when the last member leaves', async () => {
party.memberCount = 1;
it('deletes a private party when the last member leaves', async () => {
await party.leave(participatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
party = await Group.findOne({_id: party._id});
expect(party).to.not.exist;
});
it('does not delete a public group when the last member leaves', async () => {
party.memberCount = 1;
party.privacy = 'public';
await party.leave(participatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
party = await Group.findOne({_id: party._id});
expect(party).to.exist;
});
it('does not delete a private party when the member count reaches zero if there are still members', async () => {
party.memberCount = 1;
await party.leave(participatingMember);
party = await Group.findOne({_id: party._id});
expect(party).to.exist;
});
it('deletes a private guild when the last member leaves', async () => {
let guild = new Group({
name: 'test guild',
type: 'guild',
memberCount: 1,
});
let leader = new User({
guilds: [guild._id],
});
guild.leader = leader._id;
await Promise.all([
guild.save(),
leader.save(),
]);
await guild.leave(leader);
guild = await Group.findOne({_id: guild._id});
expect(guild).to.not.exist;
});
it('does not delete a private guild when the member count reaches zero if there are still members', async () => {
let guild = new Group({
name: 'test guild',
type: 'guild',
memberCount: 1,
});
let leader = new User({
guilds: [guild._id],
});
let member = new User({
guilds: [guild._id],
});
guild.leader = leader._id;
await Promise.all([
guild.save(),
leader.save(),
member.save(),
]);
await guild.leave(member);
guild = await Group.findOne({_id: guild._id});
expect(guild).to.exist;
});
});
describe('#sendChat', () => {
@@ -1061,8 +1126,45 @@ describe('Group Model', () => {
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
});
sandbox.spy(User, 'update');
describe('user update retry failures', () => {
let successfulMock = {
exec: () => {
return Promise.resolve({raw: 'sucess'});
},
};
let failedMock = {
exec: () => {
return Promise.reject(new Error('error'));
},
};
it('doesn\'t retry successful operations', async () => {
sandbox.stub(User, 'update').returns(successfulMock);
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
});
it('stops retrying when a successful update has occurred', async () => {
let updateStub = sandbox.stub(User, 'update');
updateStub.onCall(0).returns(failedMock);
updateStub.returns(successfulMock);
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
});
it('retries failed updates at most five times per user', async () => {
sandbox.stub(User, 'update').returns(failedMock);
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(10);
});
});
it('gives out achievements', async () => {
@@ -1171,13 +1273,15 @@ describe('Group Model', () => {
context('Party quests', () => {
it('updates participating members with rewards', async () => {
sandbox.spy(User, 'update');
await party.finishQuest(quest);
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledWithMatch({
_id: {
$in: [questLeader._id, participatingMember._id],
},
_id: questLeader._id,
});
expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id,
});
});
@@ -1204,6 +1308,7 @@ describe('Group Model', () => {
});
it('updates all users with rewards', async () => {
sandbox.spy(User, 'update');
await party.finishQuest(tavernQuest);
expect(User.update).to.be.calledOnce;

View File

@@ -450,4 +450,84 @@ describe('Inventory Controller', function() {
expect(scope.hasAllTimeTravelerItemsOfType('mounts')).to.eql(true);
}));
});
describe('Gear search filter', function() {
var wrap = function(text) {
return {'text': function() {return text;}};
}
var toText = function(list) {
return _.map(list, function(ele) { return ele.text(); });
}
var gearByClass, gearByType;
beforeEach(function() {
scope.$digest();
gearByClass = {'raw': [wrap('kale'), wrap('sashimi')],
'cooked': [wrap('chicken'), wrap('potato')]};
gearByType = {'veg': [wrap('kale'), wrap('potato')],
'not': [wrap('chicken'), wrap('sashimi')]};
scope.gearByClass = gearByClass;
scope.gearByType = gearByType;
scope.equipmentQuery.query = 'a';
});
it('filters nothing if equipmentQuery is nothing', function() {
scope.equipmentQuery.query = '';
scope.$digest();
expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']);
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken', 'potato']);
expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']);
expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken', 'sashimi']);
});
it('filters out gear if class gear changes', function() {
scope.$digest();
expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']);
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato']);
scope.gearByClass['raw'].push(wrap('zucchini'));
scope.gearByClass['cooked'].push(wrap('pizza'));
scope.$digest();
expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']);
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato', 'pizza']);
});
it('filters out gear if typed gear changes', function() {
scope.$digest();
expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']);
expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi']);
scope.gearByType['veg'].push(wrap('zucchini'));
scope.gearByType['not'].push(wrap('pizza'));
scope.$digest();
expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']);
expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi', 'pizza']);
});
it('filters out gear if filter query changes', function() {
scope.equipmentQuery.query = 'c';
scope.$digest();
expect(toText(scope.filteredGearByClass['raw'])).to.eql([]);
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken']);
expect(toText(scope.filteredGearByType['veg'])).to.eql([]);
expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken']);
});
it('returns the right filtered gear', function() {
var equipment = [wrap('spicy tuna'), wrap('dragon'), wrap('rainbow'), wrap('caterpillar')];
expect(toText(scope.equipmentSearch(equipment, 'ra'))).to.eql(['dragon', 'rainbow']);
});
it('returns the right filtered gear if the source gear has unicode', function() {
// blue hat, red hat, red shield
var equipment = [wrap('藍色軟帽'), wrap('紅色軟帽'), wrap('紅色盾牌')];
// searching for 'red' gives red hat, red shield
expect(toText(scope.equipmentSearch(equipment, '紅色'))).to.eql(['紅色軟帽', '紅色盾牌']);
});
});
});

View File

@@ -1,5 +1,9 @@
{
"presets": ["es2015"],
"plugins": ["transform-object-rest-spread"],
"plugins": [
"transform-object-rest-spread",
"syntax-async-functions",
"transform-regenerator",
],
"comments": false
}

View File

@@ -1,4 +1,4 @@
import { userGems } from 'client/store/getters';
import { gems as userGems } from 'client/store/getters/user';
describe('userGems getter', () => {
it('returns the user\'s gems', () => {

View File

@@ -1,6 +1,7 @@
import Vue from 'vue';
import storeInjector from 'inject?-vue!client/store';
import { mapState, mapGetters, mapActions } from 'client/store';
import { flattenAndNamespace } from 'client/store/helpers/internals';
describe('Store', () => {
let injectedStore;
@@ -14,11 +15,25 @@ describe('Store', () => {
computedName ({ state }) {
return `${state.name} computed!`;
},
...flattenAndNamespace({
nested: {
computedName ({ state }) {
return `${state.name} computed!`;
},
},
}),
},
'./actions': {
getName ({ state }, ...args) {
return [state.name, ...args];
},
...flattenAndNamespace({
nested: {
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
}),
},
}).default;
});
@@ -41,17 +56,29 @@ describe('Store', () => {
injectedStore.state.name = 'test updated';
});
it('supports getters', () => {
expect(injectedStore.getters.computedName).to.equal('test computed!');
injectedStore.state.name = 'test updated';
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
describe('getters', () => {
it('supports getters', () => {
expect(injectedStore.getters.computedName).to.equal('test computed!');
injectedStore.state.name = 'test updated';
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
});
it('supports nested getters', () => {
expect(injectedStore.getters['nested:computedName']).to.equal('test computed!');
injectedStore.state.name = 'test updated';
expect(injectedStore.getters['nested:computedName']).to.equal('test updated computed!');
});
});
describe('actions', () => {
it('can be dispatched', () => {
it('can dispatch an action', () => {
expect(injectedStore.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
});
it('can dispatch a nested action', () => {
expect(injectedStore.dispatch('nested:getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
});
it('throws an error is the action doesn\'t exists', () => {
expect(() => injectedStore.dispatched('wrong')).to.throw;
});
@@ -116,5 +143,26 @@ describe('Store', () => {
},
});
});
it('flattenAndNamespace', () => {
let result = flattenAndNamespace({
nested: {
computed ({ state }, ...args) {
return [state.name, ...args];
},
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
nested2: {
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
});
expect(Object.keys(result).length).to.equal(3);
expect(Object.keys(result).sort()).to.deep.equal(['nested2:getName', 'nested:computed', 'nested:getName']);
});
});
});

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -91,6 +91,10 @@
height: 10.5em
width: 100%
// Covers avatars, health bar at 1005-768. Fix:
@media (max-width: 1005px) and (min-width: 768px)
margin-top: 2.8em;
// this is a wrapper for avatars in the header
// inside this is the actual `herobox` module
// that can be used anywhere on the site

View File

@@ -31,6 +31,10 @@
vertical-align: bottom
margin-right: 20px
.equipment-search
max-width: 300px
margin-right: 20px
.well.use-costume-info
margin-top: 10px
p:first-child
@@ -67,6 +71,7 @@ menu.pets div
z-index: 1
padding-bottom: 0
.inventory-list
p
//display: none

View File

@@ -16,6 +16,11 @@
@extend $hrpg-button
hrpg-button-color-mixin($color-options-menu)
&.highlight
hrpg-button-color-mixin($color-options-submenu, true)
border: 1px solid;
border-radius: 3px;
.options-submenu
margin-top: -1.618em
background-color: $color-options-submenu
@@ -33,5 +38,3 @@
padding: 1em 0.618em 0 0.618em
border: 1px solid darken($color-options-menu, 12%)
border-top: none

View File

@@ -8,6 +8,7 @@ habitrpg.controller("InventoryCtrl",
$scope.selectedEgg = null; // {index: 1, name: "Tiger", value: 5}
$scope.selectedPotion = null; // {index: 5, name: "Red", value: 3}
$scope.equipmentQuery = {'query': ''};
_updateDropAnimalCount(user.items);
@@ -85,6 +86,50 @@ habitrpg.controller("InventoryCtrl",
})
}, true);
$scope.equipmentSearch = function(equipment, term) {
if (!equipment) return;
if (!angular.isString(term) || term.length == 0) {
return equipment;
}
termMatcher = new RegExp(term, 'i');
var result = [];
for (var i = 0; i < equipment.length; i++) {
if (termMatcher.test(equipment[i].text())) {
result.push(equipment[i]);
}
}
return result;
};
$scope.updateEquipment = function(gearByClass, gearByType, equipmentQuery) {
$scope.filteredGearByClass = {};
$scope.filteredGearByType = {};
_.forEach(gearByClass, function(value, key) {
var searchResult = $scope.equipmentSearch(value, equipmentQuery);
if (searchResult.length > 0) {
$scope.filteredGearByClass[key] = searchResult;
}
});
_.forEach(gearByType, function(value, key) {
var searchResult = $scope.equipmentSearch(value, equipmentQuery);
if (searchResult.length > 0) {
$scope.filteredGearByType[key] = searchResult;
}
});
}
$scope.$watch(function(){
return ['gearByClass', 'gearByType', 'equipmentQuery'].map(angular.bind($scope, $scope.$eval));
}, function(updatedVals) {
var gearByClass = updatedVals[0];
var gearByType = updatedVals[1];
var equipmentQuery = updatedVals[2];
$scope.updateEquipment(gearByClass, gearByType, equipmentQuery.query);
}, true);
$scope.updateEquipment($scope.gearByClass, $scope.gearByType, $scope.equipmentQuery.query);
$scope.chooseEgg = function(egg){
if ($scope.selectedEgg && $scope.selectedEgg.key == egg) {
return $scope.selectedEgg = null; // clicked same egg, unselect

View File

@@ -18,6 +18,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$timeout(function () {
if (window.env.IS_MOBILE || User.user.preferences.stickyHeader === false) return;
$('.header-wrap').sticky({topSpacing:0});
$(window).resize(function() {$('.header-wrap').sticky('update');});
});
// Remove listener

View File

@@ -119,6 +119,9 @@ angular.module('habitrpg')
Groups.data.party = party;
$state.go('options.social.party');
resolve();
if ($state.current.name === "options.social.party") {
$state.reload();
}
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -1,4 +1,8 @@
{
"presets": ["es2015"],
"plugins": ["transform-object-rest-spread"],
"plugins": [
"transform-object-rest-spread",
"syntax-async-functions",
"transform-regenerator",
],
}

File diff suppressed because it is too large Load Diff

View File

@@ -213,7 +213,9 @@ import { mapState, mapGetters } from '../store';
export default {
computed: {
...mapGetters(['userGems']),
...mapGetters({
userGems: 'user:gems',
}),
...mapState(['user']),
},
};

View File

@@ -44,8 +44,8 @@ let userDataWatcher = store.watch(state => [state.user, state.tasks], ([user, ta
// Load the user and the user tasks
Promise.all([
store.dispatch('user.fetch'),
store.dispatch('tasks.fetchUserTasks'),
store.dispatch('user:fetch'),
store.dispatch('tasks:fetchUserTasks'),
]).catch(() => {
alert('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.');
});

View File

@@ -1,9 +1,13 @@
import tasks from './tasks';
import user from './user';
import { flattenAndNamespace } from '../helpers/internals';
import * as tasks from './tasks';
import * as user from './user';
const actions = {
tasks,
// Actions should be named as 'actionName' and can be accessed as 'namespace.actionName'
// Example: fetch in user.js -> 'user.fetch'
const actions = flattenAndNamespace({
user,
};
tasks,
});
export default actions;

View File

@@ -1,15 +1,6 @@
import Vue from 'vue';
const actions = {};
actions.fetchUserTasks = function fetchUserTasks (store) {
let promise = Vue.http.get('/api/v3/tasks/user');
promise.then((response) => {
store.state.tasks = response.body.data;
});
return promise;
};
export default actions;
export async function fetchUserTasks (store) {
let response = await Vue.http.get('/api/v3/tasks/user');
store.state.tasks = response.body.data;
}

View File

@@ -1,15 +1,6 @@
import Vue from 'vue';
const actions = {};
actions.fetch = function fetchUser (store) {
let promise = Vue.http.get('/api/v3/user');
promise.then((response) => {
store.state.user = response.body.data;
});
return promise;
};
export default actions;
export async function fetch (store) { // eslint-disable-line no-shadow
let response = await Vue.http.get('/api/v3/user');
store.state.user = response.body.data;
}

View File

@@ -1,3 +1,11 @@
export function userGems (store) {
return store.state.user.balance * 4;
}
import { flattenAndNamespace } from '../helpers/internals';
import * as user from './user';
// Getters should be named as 'getterName' and can be accessed as 'namespace.getterName'
// Example: gems in user.js -> 'user.gems'
const getters = flattenAndNamespace({
user,
});
export default getters;

View File

@@ -0,0 +1,3 @@
export function gems (store) {
return store.state.user.balance * 4;
}

View File

@@ -0,0 +1,32 @@
/* Flatten multiple objects into a single, namespaced object.
Example:
getters
user
gems
tasks
...
tasks
todos
dailys
...
Result:
getters
user.gems
user.tasks
tasks.todos
tasks.dailys
*/
export function flattenAndNamespace (namespaces) {
let result = {};
Object.keys(namespaces).forEach(namespace => {
Object.keys(namespaces[namespace]).forEach(itemName => {
result[`${namespace}:${itemName}`] = namespaces[namespace][itemName];
});
});
return result;
}

View File

@@ -1,8 +1,7 @@
import Vue from 'vue';
import state from './state';
import actions from './actions';
import * as getters from './getters';
import { get } from 'lodash';
import getters from './getters';
// Central application store for Habitica
// Heavily inspired to Vuex (https://github.com/vuejs/vuex) with a very
@@ -22,7 +21,7 @@ const store = {
// Actions should be called using store.dispatch(ACTION_NAME, ...ARGS)
// They get passed the store instance and any additional argument passed to dispatch()
dispatch (type, ...args) {
let action = get(actions, type);
let action = actions[type];
if (!action) throw new Error(`Action "${type}" not found.`);
return action(store, ...args);
@@ -60,7 +59,7 @@ export {
mapState,
mapGetters,
mapActions,
} from './helpers';
} from './helpers/public';
// Setup internal Vue instance to make state and getters reactive
_vm = new Vue({

View File

@@ -1,12 +1,19 @@
{
"merch" : "Merchandise",
"merchandiseDescription": "Looking for t-shirts, mugs, or stickers to show off your Habitica pride? Click here!",
"merch-teespring-summary" : "Teespring is a platform that makes it easy for anyone to create and sell high-quality products people love, with no cost or risk.",
"merch-teespring-goto" : "Get a Habitica T-shirt",
"merch-teespring-mug-summary" : "Teespring is a platform that makes it easy for anyone to create and sell high-quality products people love, with no cost or risk.",
"merch-teespring-mug-goto" : "Get a Habitica Mug",
"merch-teespring-eu-summary" : "EUROPEAN VERSION : Teespring is a platform that makes it easy for anyone to create and sell high-quality products people love, with no cost or risk.",
"merch-teespring-eu-goto" : "Get a Habitica T-shirt (EU)",
"merch-teespring-mug-eu-summary" : "EUROPEAN VERSION : Teespring is a platform that makes it easy for anyone to create and sell high-quality products people love, with no cost or risk.",
"merch-teespring-mug-eu-goto" : "Get a Habitica Mug (EU)",
"merch-stickermule-summary" : "Stick proud Melior wherever you (or someone else) need a reminder of both present and future accomplishments!",
"merch-stickermule-goto" : "Get Habitica stickers"

View File

@@ -92,5 +92,7 @@
"petsReleased": "Pets released.",
"mountsAndPetsReleased": "Mounts and pets released",
"mountsReleased": "Mounts released",
"gemsEach": "gems each"
"gemsEach": "gems each",
"foodWikiText": "What does my pet like to eat?",
"foodWikiUrl": "http://habitica.wikia.com/wiki/Food_Preferences"
}

View File

@@ -80,6 +80,7 @@ api.createSubscription = async function createSubscription (data) {
plan.dateTerminated = moment(plan.dateTerminated).add({months}).toDate();
} else {
plan.dateTerminated = moment().add({months}).toDate();
plan.dateCreated = today;
}
}

View File

@@ -38,6 +38,7 @@ const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = shared.constants.LARGE_GROUP_COUNT_MESS
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
const MAX_UPDATE_RETRIES = 5;
export let schema = new Schema({
name: {type: String, required: true},
@@ -577,6 +578,19 @@ schema.statics.cleanGroupQuest = function cleanGroupQuest () {
};
};
async function _updateUserWithRetries (userId, updates, numTry = 1) {
return await User.update({_id: userId}, updates).exec()
.then((raw) => {
return raw;
}).catch((err) => {
if (numTry < MAX_UPDATE_RETRIES) {
return _updateUserWithRetries(userId, updates, ++numTry);
} else {
throw err;
}
});
}
// Participants: Grant rewards & achievements, finish quest.
// Changes the group object update members
schema.methods.finishQuest = async function finishQuest (quest) {
@@ -623,11 +637,19 @@ schema.methods.finishQuest = async function finishQuest (quest) {
}
});
let q = this._id === TAVERN_ID ? {} : {_id: {$in: this.getParticipatingQuestMembers()}};
let participants = this._id === TAVERN_ID ? {} : this.getParticipatingQuestMembers();
this.quest = {};
this.markModified('quest');
return await User.update(q, updates, {multi: true}).exec();
if (this._id === TAVERN_ID) {
return await User.update({}, updates, {multi: true}).exec();
}
let promises = participants.map(userId => {
return _updateUserWithRetries(userId, updates);
});
return Bluebird.all(promises);
};
function _isOnQuest (user, progress, group) {
@@ -835,9 +857,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
let group = this;
let update = {
$inc: {memberCount: -1},
};
let update = {};
let challenges = await Challenge.find({
_id: {$in: user.challenges},
@@ -867,18 +887,30 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
// If user is the last one in group and group is private, delete it
if (group.memberCount <= 1 && group.privacy === 'private') {
promises.push(group.remove());
} else { // otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
if (group.leader === user._id) {
let query = group.type === 'party' ? {'party._id': group._id} : {guilds: group._id};
query._id = {$ne: user._id};
let seniorMember = await User.findOne(query).select('_id').exec();
// could be missing in case of public guild (that can have 0 members) with 1 member who is leaving
if (seniorMember) update.$set = {leader: seniorMember._id};
// double check the member count is correct so we don't accidentally delete a group that still has users in it
let members;
if (group.type === 'guild') {
members = await User.find({guilds: group._id}).select('_id').exec();
} else {
members = await User.find({'party._id': group._id}).select('_id').exec();
}
_.remove(members, {_id: user._id});
if (members.length === 0) {
promises.push(group.remove());
return await Bluebird.all(promises);
}
promises.push(group.update(update).exec());
}
// otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
update.$inc = {memberCount: -1};
if (group.leader === user._id) {
let query = group.type === 'party' ? {'party._id': group._id} : {guilds: group._id};
query._id = {$ne: user._id};
let seniorMember = await User.findOne(query).select('_id').exec();
// could be missing in case of public guild (that can have 0 members) with 1 member who is leaving
if (seniorMember) update.$set = {leader: seniorMember._id};
}
promises.push(group.update(update).exec());
return await Bluebird.all(promises);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -31,16 +31,22 @@ mixin equipmentButton(type)
popover-append-to-body='true')
mixin equipmentList(equipmentType)
menu.pets-menu(label='{{::label}}', ng-show='gearByClass[klass]', ng-if='groupingChoice === "klass"',
menu.pets-menu(label='{{::label}}', ng-show='filteredGearByClass[klass]', ng-if='groupingChoice === "klass"',
ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery"), armoire:env.t("armoireText")}')
div(ng-repeat='item in gearByClass[klass] | orderBy: order')
div(ng-repeat='item in filteredGearByClass[klass] | orderBy: order')
+equipmentButton(equipmentType)
menu.pets-menu(label='{{::label}}', ng-show='gearByType[type]', ng-if='groupingChoice === "equipmentType"',
menu.pets-menu(label='{{::label}}', ng-show='filteredGearByType[type]', ng-if='groupingChoice === "equipmentType"',
ng-repeat='(type,label) in {headAccessory:env.t("headAccessoryCapitalized"), head:env.t("headgearCapitalized"), eyewear:env.t("eyewear"), weapon:env.t("weaponCapitalized"), shield:env.t("offhandCapitalized"), armor:env.t("armorCapitalized"), body:env.t("body"), back:env.t("back")}')
div(ng-repeat='item in gearByType[type] | orderBy: order', ng-show='item.klass !== "base"')
div(ng-repeat='item in filteredGearByType[type] | orderBy: order', ng-show='item.klass !== "base"')
+equipmentButton(equipmentType)
.container-fluid
.row
.col-md-6
.input-group.equipment-search
.input-group-addon
.glyphicon.glyphicon-search
input.form-control(type='text', placeholder=env.t('search'), ng-model='equipmentQuery.query', ng-model-options='{ debounce: 250 }')
.row
.col-md-6.border-right(ng-controller="SortableInventoryController")
h3.equipment-title.hint(popover-trigger='mouseenter',

View File

@@ -20,6 +20,8 @@ ul.options-menu
li(ng-class="{ active: $state.includes('options.inventory.seasonalshop') }")
a(ui-sref='options.inventory.seasonalshop')
=env.t('seasonalShop')
li.pull-right.highlight(tooltip=env.t('merchandiseDescription'))
a(href='/static/merch')=env.t('merch')
.tab-content
.tab-pane.active

View File

@@ -62,6 +62,7 @@ mixin petList(eggSource, potionSource)
menu.inventory-list(type='list', ng-if='foodCount > 0')
li.customize-menu
menu.pets-menu(label=env.t('food'))
a(href=env.t('foodWikiUrl'), target='_blank')=env.t('foodWikiText')
div(ng-repeat='(food,points) in ownedItems(user.items.food)')
button.customize-option(popover-append-to-body='true', popover='{{:: Content.food[food].notes()}}', popover-title='{{:: Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='top', ng-click='chooseFood(food)', ng-class='{selectableInventory: selectedFood == Content.food[food]}', class='Pet_Food_{{::food}}')
.badge.badge-info.stack-count {{points}}

View File

@@ -1,42 +1,55 @@
h2 12/8/2016 - GIFT-1-GET-1 SUBSCRIPTIONS, NEW PET QUEST, AND HOLIDAY PREP TIPS
h2 12/12/2016 - LAST CHANCE FOR HABITICA T-SHIRTS AND MUGS; STAFF SPOTLIGHT
hr
tr
td
.inventory_present_01.pull-left.slight-right-margin
h3 Gift a Subscription and Get One Free!
p In honor of the season of giving, we're running a special promotion for the next month only. Now when you gift somebody <a href='/#/options/settings/subscription'>a subscription</a>, you get the same subscription for yourself for free!
br
p Subscribers get tons of perks every month, including exclusive items, the ability to buy gems with gold, and increased data history. Plus, it helps keep Habitica running :) To gift a subscription to someone, just open their profile and click on the present icon in the lower-left.
br
p The special promotion will only run until January 6th, so if you've been curious about trying out a subscription, now's the time! Make a friend happy and use all your new gems to go questing together.
p.small.muted by SabreCat and Lemoness
.promo_coffee_mug.pull-left.slight-right-margin
h3 Last Chance for Habitica T-Shirts and Mugs!
p Today's the final day to get <a href='https://teespring.com/stores/habitica' target='_blank'>our Habitica T-shirts and mugs</a> if you live in the EU, and tomorrow's the final day if you live in the USA, so if you want one, be sure to grab it now! Thanks again for all your support -- you're all awesome.
.promo_contrib_spotlight_Keith.pull-right
p.small.muted by Redphoenix and Beffymaroo
tr
td
span.Mount_Body_Sloth-Base.pull-right
span.Mount_Head_Sloth-Base.pull-right(style='margin:0')
h3 New Pet Quest: Sloth!
p The Somnolent Sloth is making everyone sleepy! Can you shake off the snow and get stuff done? Get the latest pet quest, <a href='/#/options/inventory/quests'>The Somnolent Sloth</a>, and earn some speedy sloth pets by completing your real-life tasks.
p.small.muted Written by PixelHunter
p.small.muted Art by JaizakAripaik, Drevian, McCoyly, awakebyjava, PainterProphet, Kiwibot, and greenpencil
tr
td
.promo_winter_fireworks.pull-left.slight-right-margin
h3 Guild Spotlight: Preparing for the Holidays
p There's a new <a href='https://habitica.wordpress.com/2016/11/29/prepping-for-the-holidays/' target='_blank'>Guild Spotlight on the blog</a> that highlights the Guilds that can help you prepare for the holidays! Check it out now to find Habitican communities that can keep you stress-free as seasonal celebrations approach.
p.small.muted by Beffymaroo
tr
td
h3 Use Case Spotlight: Holiday Survival Tips
p This month's <a href='https://habitica.wordpress.com/2016/12/06/use-case-spotlight-prepping-for-the-holidays/' target='_blank'>Use Case Spotlight</a> is about ways to use Habitica to make the holidays more enjoyable! It features a number of great suggestions submitted by Habiticans in the <a href='/#/options/groups/guilds/1d3a10bf-60aa-4806-a38b-82d1084a59e6'>Use Case Spotlights Guild</a>. We hope it helps you!
br
p Plus, we're collecting user submissions for the next spotlight! How do you use Habitica to manage your health and fitness? Well be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog at the start of next month, so post your suggestions in the Use Case Spotlight Guild now!
p.small.muted by Beffymaroo
h3 Staff Spotlight: Keith
p We've posted a new <a href='https://habitica.wordpress.com/2016/12/09/staff-spotlight-keith-aka-thehollidayinn/' target='_blank'>Staff Spotlight</a> on the blog! Check out our latest interview with Keith AKA TheHollidayInn, our newest staff member and awesome engineer.
p.small.muted by Lemoness, Beffymaroo, and TheHollidayInn
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 12/8/2016 - GIFT-1-GET-1 SUBSCRIPTIONS, NEW PET QUEST, AND HOLIDAY PREP TIPS
tr
td
.inventory_present_01.pull-left.slight-right-margin
h3 Gift a Subscription and Get One Free!
p In honor of the season of giving, we're running a special promotion for the next month only. Now when you gift somebody <a href='/#/options/settings/subscription'>a subscription</a>, you get the same subscription for yourself for free!
br
p Subscribers get tons of perks every month, including exclusive items, the ability to buy gems with gold, and increased data history. Plus, it helps keep Habitica running :) To gift a subscription to someone, just open their profile and click on the present icon in the lower-left.
br
p The special promotion will only run until January 6th, so if you've been curious about trying out a subscription, now's the time! Make a friend happy and use all your new gems to go questing together.
p.small.muted by SabreCat and Lemoness
tr
td
span.Mount_Body_Sloth-Base.pull-right
span.Mount_Head_Sloth-Base.pull-right(style='margin:0')
h3 New Pet Quest: Sloth!
p The Somnolent Sloth is making everyone sleepy! Can you shake off the snow and get stuff done? Get the latest pet quest, <a href='/#/options/inventory/quests'>The Somnolent Sloth</a>, and earn some speedy sloth pets by completing your real-life tasks.
p.small.muted Written by PixelHunter
p.small.muted Art by JaizakAripaik, Drevian, McCoyly, awakebyjava, PainterProphet, Kiwibot, and greenpencil
tr
td
.promo_winter_fireworks.pull-left.slight-right-margin
h3 Guild Spotlight: Preparing for the Holidays
p There's a new <a href='https://habitica.wordpress.com/2016/11/29/prepping-for-the-holidays/' target='_blank'>Guild Spotlight on the blog</a> that highlights the Guilds that can help you prepare for the holidays! Check it out now to find Habitican communities that can keep you stress-free as seasonal celebrations approach.
p.small.muted by Beffymaroo
tr
td
h3 Use Case Spotlight: Holiday Survival Tips
p This month's <a href='https://habitica.wordpress.com/2016/12/06/use-case-spotlight-prepping-for-the-holidays/' target='_blank'>Use Case Spotlight</a> is about ways to use Habitica to make the holidays more enjoyable! It features a number of great suggestions submitted by Habiticans in the <a href='/#/options/groups/guilds/1d3a10bf-60aa-4806-a38b-82d1084a59e6'>Use Case Spotlights Guild</a>. We hope it helps you!
br
p Plus, we're collecting user submissions for the next spotlight! How do you use Habitica to manage your health and fitness? Well be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog at the start of next month, so post your suggestions in the Use Case Spotlight Guild now!
p.small.muted by Beffymaroo
h2 12/1/2016 - NEW BACKGROUNDS AND ARMOIRE ITEMS, iOS UPDATE, TAKE THIS CHALLENGE, AND HABITICA STAFF MEMBER
.promo_backgrounds_armoire_201612.pull-right
tr

View File

@@ -19,6 +19,8 @@ block vars
- var merchants = []
- merchants.push({key: 'teespring', logo: true, name: 'Teespring', link: 'https://teespring.com/habitica-gryphon-t-shirt'})
- merchants.push({key: 'teespring-eu', logo: true, name: 'Teespring (EU)', link: 'https://teespring.com/habitica-gryphon-t-shirt_eu'})
- merchants.push({key: 'teespring-mug', logo: true, name: 'Teespring', link: 'https://teespring.com/shop/habitica-gryphon-mug'})
- merchants.push({key: 'teespring-mug-eu', logo: true, name: 'Teespring (EU)', link: 'https://teespring.com/shop/habitica-hydration-mug-eu'})
- merchants.push({key: 'stickermule', logo: true, name: 'Stickermule', link: 'https://www.stickermule.com/uk/marketplace/9317-habitica-gryphon-sticker'})
block title