mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
Remove localstorage and add notifications (#7588)
* move remaining files frm /common/script/public to website/public * remove localstorage * add back noscript template and put all javascript in the footer * fixes client side tests * remove double quotes where possible * simplify jade code and add tests for buildManifest * loading page with logo and spinner * better loading screen in landscape mode * icon on top of text logo * wip: user.notifications * notifications: simpler and working code * finish implementing notifications * correct loading screen css and re-inline images * add tests for user notifications * split User model in multiple files * remove old comment about missing .catch() * correctly setup hooks and methods for User model. Cleanup localstorage * include UserNotificationsService in static page js and split loading-screen css in its own file * add cron notification and misc fixes * remove console.log * fix tests * fix multiple notifications
This commit is contained in:
@@ -58,5 +58,6 @@
|
|||||||
"messageGroupChatAdminClearFlagCount": "Only an admin can clear the flag count!",
|
"messageGroupChatAdminClearFlagCount": "Only an admin can clear the flag count!",
|
||||||
|
|
||||||
"messageUserOperationProtected": "path `<%= operation %>` was not saved, as it's a protected path.",
|
"messageUserOperationProtected": "path `<%= operation %>` was not saved, as it's a protected path.",
|
||||||
"messageUserOperationNotFound": "<%= operation %> operation not found"
|
"messageUserOperationNotFound": "<%= operation %> operation not found",
|
||||||
|
"messageNotificationNotFound": "Notification not found."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export const MAX_HEALTH = 50;
|
|||||||
export const MAX_LEVEL = 100;
|
export const MAX_LEVEL = 100;
|
||||||
export const MAX_STAT_POINTS = MAX_LEVEL;
|
export const MAX_STAT_POINTS = MAX_LEVEL;
|
||||||
export const ATTRIBUTES = ['str', 'int', 'per', 'con'];
|
export const ATTRIBUTES = ['str', 'int', 'per', 'con'];
|
||||||
export const TAVERN_ID = '00000000-0000-4000-A000-000000000000';
|
|
||||||
|
export const TAVERN_ID = '00000000-0000-4000-A000-000000000000';
|
||||||
@@ -12,6 +12,10 @@ module.exports = function ultimateGear (user) {
|
|||||||
});
|
});
|
||||||
return soFarGood && (!found || owned[found.key] === true);
|
return soFarGood && (!found || owned[found.key] === true);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
if (user.achievements.ultimateGearSets[klass] === true) {
|
||||||
|
user.addNotification('ULTIMATE_GEAR_ACHIEVEMENT');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ module.exports = function updateStats (user, stats, req = {}, analytics) {
|
|||||||
}
|
}
|
||||||
if (!user.flags.dropsEnabled && user.stats.lvl >= 3) {
|
if (!user.flags.dropsEnabled && user.stats.lvl >= 3) {
|
||||||
user.flags.dropsEnabled = true;
|
user.flags.dropsEnabled = true;
|
||||||
|
user.addNotification('DROPS_ENABLED');
|
||||||
|
|
||||||
if (user.items.eggs.Wolf > 0) {
|
if (user.items.eggs.Wolf > 0) {
|
||||||
user.items.eggs.Wolf++;
|
user.items.eggs.Wolf++;
|
||||||
} else {
|
} else {
|
||||||
@@ -92,6 +94,7 @@ module.exports = function updateStats (user, stats, req = {}, analytics) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!user.flags.rebirthEnabled && (user.stats.lvl >= 50 || user.achievements.beastMaster)) {
|
if (!user.flags.rebirthEnabled && (user.stats.lvl >= 50 || user.achievements.beastMaster)) {
|
||||||
|
user.addNotification('REBIRTH_ENABLED');
|
||||||
user.flags.rebirthEnabled = true;
|
user.flags.rebirthEnabled = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -242,6 +242,11 @@ api.wrap = function wrapUser (user, main = true) {
|
|||||||
user.markModified = function noopMarkModified () {};
|
user.markModified = function noopMarkModified () {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// same for addNotification
|
||||||
|
if (!user.addNotification) {
|
||||||
|
user.addNotification = function noopAddNotification () {};
|
||||||
|
}
|
||||||
|
|
||||||
if (main) {
|
if (main) {
|
||||||
user.ops = {
|
user.ops = {
|
||||||
update: _.partial(importedOps.update, user),
|
update: _.partial(importedOps.update, user),
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import openMysteryItem from './openMysteryItem';
|
|||||||
import scoreTask from './scoreTask';
|
import scoreTask from './scoreTask';
|
||||||
import markPmsRead from './markPMSRead';
|
import markPmsRead from './markPMSRead';
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
update,
|
update,
|
||||||
sleep,
|
sleep,
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ module.exports = function rebirth (user, tasks = [], req = {}, analytics) {
|
|||||||
user.achievements.rebirthLevel = lvl;
|
user.achievements.rebirthLevel = lvl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.addNotification('REBIRTH_ACHIEVEMENT');
|
||||||
|
|
||||||
user.stats.buffs = {};
|
user.stats.buffs = {};
|
||||||
|
|
||||||
if (req.v2 === true) {
|
if (req.v2 === true) {
|
||||||
|
|||||||
@@ -214,7 +214,10 @@ module.exports = function scoreTask (options = {}, req = {}) {
|
|||||||
if (direction === 'up') {
|
if (direction === 'up') {
|
||||||
task.streak += 1;
|
task.streak += 1;
|
||||||
// Give a streak achievement when the streak is a multiple of 21
|
// Give a streak achievement when the streak is a multiple of 21
|
||||||
if (task.streak % 21 === 0) user.achievements.streak = user.achievements.streak ? user.achievements.streak + 1 : 1;
|
if (task.streak % 21 === 0) {
|
||||||
|
user.achievements.streak = user.achievements.streak ? user.achievements.streak + 1 : 1;
|
||||||
|
user.addNotification('STREAK_ACHIEVEMENT');
|
||||||
|
}
|
||||||
task.completed = true;
|
task.completed = true;
|
||||||
} else if (direction === 'down') {
|
} else if (direction === 'down') {
|
||||||
// Remove a streak achievement if streak was a multiple of 21 and the daily was undone
|
// Remove a streak achievement if streak was a multiple of 21 and the daily was undone
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
|
|||||||
await sleep(0.5);
|
await sleep(0.5);
|
||||||
|
|
||||||
await expect(winningUser.sync()).to.eventually.have.deep.property('achievements.challenges').to.include(challenge.name);
|
await expect(winningUser.sync()).to.eventually.have.deep.property('achievements.challenges').to.include(challenge.name);
|
||||||
|
expect(winningUser.notifications.length).to.equal(1);
|
||||||
|
expect(winningUser.notifications[0].type).to.equal('WON_CHALLENGE');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gives winner gems as reward', async () => {
|
it('gives winner gems as reward', async () => {
|
||||||
|
|||||||
@@ -17,16 +17,19 @@ describe('GET /export/history.csv', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// score all the tasks twice
|
// score all the tasks twice
|
||||||
await Promise.all(tasks.map(task => {
|
await user.post(`/tasks/${tasks[0]._id}/score/up`);
|
||||||
return user.post(`/tasks/${task._id}/score/up`);
|
await user.post(`/tasks/${tasks[1]._id}/score/up`);
|
||||||
}));
|
await user.post(`/tasks/${tasks[2]._id}/score/up`);
|
||||||
await Promise.all(tasks.map(task => {
|
await user.post(`/tasks/${tasks[3]._id}/score/up`);
|
||||||
return user.post(`/tasks/${task._id}/score/up`);
|
|
||||||
}));
|
await user.post(`/tasks/${tasks[0]._id}/score/up`);
|
||||||
|
await user.post(`/tasks/${tasks[1]._id}/score/up`);
|
||||||
|
await user.post(`/tasks/${tasks[2]._id}/score/up`);
|
||||||
|
await user.post(`/tasks/${tasks[3]._id}/score/up`);
|
||||||
|
|
||||||
// adding an history entry to daily 1 manually because cron didn't run yet
|
// adding an history entry to daily 1 manually because cron didn't run yet
|
||||||
await updateDocument('tasks', tasks[1], {
|
await updateDocument('tasks', tasks[1], {
|
||||||
history: {value: 3.2, date: Number(new Date())},
|
history: [{value: 3.2, date: Number(new Date())}],
|
||||||
});
|
});
|
||||||
|
|
||||||
// get updated tasks
|
// get updated tasks
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ describe('PUT /heroes/:heroId', () => {
|
|||||||
expect(hero.contributor.level).to.equal(1);
|
expect(hero.contributor.level).to.equal(1);
|
||||||
expect(hero.purchased.ads).to.equal(true);
|
expect(hero.purchased.ads).to.equal(true);
|
||||||
expect(hero.auth.blocked).to.equal(true);
|
expect(hero.auth.blocked).to.equal(true);
|
||||||
|
expect(hero.notifications.length).to.equal(1);
|
||||||
|
expect(hero.notifications[0].type).to.equal('NEW_CONTRIBUTOR_LEVEL');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates contributor level', async () => {
|
it('updates contributor level', async () => {
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ describe('POST /user/rebirth', () => {
|
|||||||
let response = await user.post('/user/rebirth');
|
let response = await user.post('/user/rebirth');
|
||||||
await user.sync();
|
await user.sync();
|
||||||
|
|
||||||
|
expect(user.notifications.length).to.equal(1);
|
||||||
|
expect(user.notifications[0].type).to.equal('REBIRTH_ACHIEVEMENT');
|
||||||
|
|
||||||
let updatedDaily = await user.get(`/tasks/${daily._id}`);
|
let updatedDaily = await user.get(`/tasks/${daily._id}`);
|
||||||
let updatedReward = await user.get(`/tasks/${reward._id}`);
|
let updatedReward = await user.get(`/tasks/${reward._id}`);
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ describe('PUT /user', () => {
|
|||||||
backer: {'backer.tier': 10, 'backer.npc': 'Bilbo'},
|
backer: {'backer.tier': 10, 'backer.npc': 'Bilbo'},
|
||||||
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
|
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
|
||||||
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
|
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
|
||||||
|
notifications: [{type: 123}],
|
||||||
};
|
};
|
||||||
|
|
||||||
each(protectedOperations, (data, testName) => {
|
each(protectedOperations, (data, testName) => {
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ describe('Build Manifest', () => {
|
|||||||
expect(htmlCode.startsWith('<script') || htmlCode.startsWith('<link')).to.be.true;
|
expect(htmlCode.startsWith('<script') || htmlCode.startsWith('<link')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can return only js files', () => {
|
||||||
|
let htmlCode = getManifestFiles('app', 'js');
|
||||||
|
|
||||||
|
expect(htmlCode.indexOf('<link') === -1).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can return only css files', () => {
|
||||||
|
let htmlCode = getManifestFiles('app', 'css');
|
||||||
|
|
||||||
|
expect(htmlCode.indexOf('<script') === -1).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
it('throws an error in case the page does not exist', () => {
|
it('throws an error in case the page does not exist', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getManifestFiles('strange name here');
|
getManifestFiles('strange name here');
|
||||||
|
|||||||
@@ -517,6 +517,60 @@ describe('cron', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('notifications', () => {
|
||||||
|
it('adds a user notification', () => {
|
||||||
|
let mpBefore = user.stats.mp;
|
||||||
|
tasksByType.dailys[0].completed = true;
|
||||||
|
user._statsComputed.maxMP = 100;
|
||||||
|
|
||||||
|
daysMissed = 1;
|
||||||
|
let hpBefore = user.stats.hp;
|
||||||
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.notifications.length).to.equal(1);
|
||||||
|
expect(user.notifications[0].type).to.equal('CRON');
|
||||||
|
expect(user.notifications[0].data).to.eql({
|
||||||
|
hp: user.stats.hp - hpBefore,
|
||||||
|
mp: user.stats.mp - mpBefore,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('condenses multiple notifications into one', () => {
|
||||||
|
let mpBefore1 = user.stats.mp;
|
||||||
|
tasksByType.dailys[0].completed = true;
|
||||||
|
user._statsComputed.maxMP = 100;
|
||||||
|
|
||||||
|
daysMissed = 1;
|
||||||
|
let hpBefore1 = user.stats.hp;
|
||||||
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.notifications.length).to.equal(1);
|
||||||
|
expect(user.notifications[0].type).to.equal('CRON');
|
||||||
|
expect(user.notifications[0].data).to.eql({
|
||||||
|
hp: user.stats.hp - hpBefore1,
|
||||||
|
mp: user.stats.mp - mpBefore1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let hpBefore2 = user.stats.hp;
|
||||||
|
let mpBefore2 = user.stats.mp;
|
||||||
|
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.notifications.length).to.equal(1);
|
||||||
|
expect(user.notifications[0].type).to.equal('CRON');
|
||||||
|
expect(user.notifications[0].data).to.eql({
|
||||||
|
hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1),
|
||||||
|
mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('private messages', () => {
|
describe('private messages', () => {
|
||||||
let lastMessageId;
|
let lastMessageId;
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ describe('response middleware', () => {
|
|||||||
expect(res.json).to.be.calledWith({
|
expect(res.json).to.be.calledWith({
|
||||||
success: true,
|
success: true,
|
||||||
data: {field: 1},
|
data: {field: 1},
|
||||||
|
notifications: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ describe('response middleware', () => {
|
|||||||
success: true,
|
success: true,
|
||||||
data: {field: 1},
|
data: {field: 1},
|
||||||
message: 'hello',
|
message: 'hello',
|
||||||
|
notifications: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,6 +63,45 @@ describe('response middleware', () => {
|
|||||||
expect(res.json).to.be.calledWith({
|
expect(res.json).to.be.calledWith({
|
||||||
success: false,
|
success: false,
|
||||||
data: {field: 1},
|
data: {field: 1},
|
||||||
|
notifications: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns userV if a user is authenticated req.query.userV is passed', () => {
|
||||||
|
responseMiddleware(req, res, next);
|
||||||
|
req.query.userV = 3;
|
||||||
|
res.respond(200, {field: 1});
|
||||||
|
|
||||||
|
expect(res.json).to.be.calledOnce;
|
||||||
|
|
||||||
|
expect(res.json).to.be.calledWith({
|
||||||
|
success: true,
|
||||||
|
data: {field: 1},
|
||||||
|
notifications: [],
|
||||||
|
userV: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns notifications if a user is authenticated', () => {
|
||||||
|
res.locals.user.notifications.push({type: 'NEW_CONTRIBUTOR_LEVEL'});
|
||||||
|
let notification = res.locals.user.notifications[0].toJSON();
|
||||||
|
|
||||||
|
responseMiddleware(req, res, next);
|
||||||
|
res.respond(200, {field: 1});
|
||||||
|
|
||||||
|
expect(res.json).to.be.calledOnce;
|
||||||
|
|
||||||
|
expect(res.json).to.be.calledWith({
|
||||||
|
success: true,
|
||||||
|
data: {field: 1},
|
||||||
|
notifications: [
|
||||||
|
{
|
||||||
|
type: notification.type,
|
||||||
|
id: notification.id,
|
||||||
|
createdAt: notification.createdAt,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,4 +30,30 @@ describe('User Model', () => {
|
|||||||
expect(toJSON._tmp).to.eql({ok: true});
|
expect(toJSON._tmp).to.eql({ok: true});
|
||||||
expect(toJSON).to.not.have.keys('_nonTmp');
|
expect(toJSON).to.not.have.keys('_nonTmp');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('notifications', () => {
|
||||||
|
it('can add notifications with data', () => {
|
||||||
|
let user = new User();
|
||||||
|
|
||||||
|
user.addNotification('CRON');
|
||||||
|
|
||||||
|
let userToJSON = user.toJSON();
|
||||||
|
expect(user.notifications.length).to.equal(1);
|
||||||
|
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'createdAt']);
|
||||||
|
expect(userToJSON.notifications[0].type).to.equal('CRON');
|
||||||
|
expect(userToJSON.notifications[0].data).to.eql({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add notifications without data', () => {
|
||||||
|
let user = new User();
|
||||||
|
|
||||||
|
user.addNotification('CRON', {field: 1});
|
||||||
|
|
||||||
|
let userToJSON = user.toJSON();
|
||||||
|
expect(user.notifications.length).to.equal(1);
|
||||||
|
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'createdAt']);
|
||||||
|
expect(userToJSON.notifications[0].type).to.equal('CRON');
|
||||||
|
expect(userToJSON.notifications[0].data).to.eql({field: 1});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ describe('shared.fns.ultimateGear', () => {
|
|||||||
user.achievements.ultimateGearSets.toObject = function () {
|
user.achievements.ultimateGearSets.toObject = function () {
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
user.addNotification = sinon.spy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets armoirEnabled when partial achievement already achieved', () => {
|
it('sets armoirEnabled when partial achievement already achieved', () => {
|
||||||
@@ -31,7 +32,10 @@ describe('shared.fns.ultimateGear', () => {
|
|||||||
|
|
||||||
user.items = items;
|
user.items = items;
|
||||||
ultimateGear(user);
|
ultimateGear(user);
|
||||||
|
|
||||||
expect(user.flags.armoireEnabled).to.equal(true);
|
expect(user.flags.armoireEnabled).to.equal(true);
|
||||||
|
expect(user.addNotification).to.be.calledOnce;
|
||||||
|
expect(user.addNotification).to.be.calledWith('ULTIMATE_GEAR_ACHIEVEMENT');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not set armoirEnabled when gear is not owned', () => {
|
it('does not set armoirEnabled when gear is not owned', () => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ describe('common.fns.updateStats', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = generateUser();
|
user = generateUser();
|
||||||
|
user.addNotification = sinon.spy();
|
||||||
});
|
});
|
||||||
|
|
||||||
context('No Hp', () => {
|
context('No Hp', () => {
|
||||||
@@ -109,6 +110,20 @@ describe('common.fns.updateStats', () => {
|
|||||||
expect(user.stats.points).to.eql(10);
|
expect(user.stats.points).to.eql(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('add user notification when drops are enabled', () => {
|
||||||
|
user.stats.lvl = 3;
|
||||||
|
updateStats(user, { });
|
||||||
|
expect(user.addNotification).to.be.calledOnce;
|
||||||
|
expect(user.addNotification).to.be.calledWith('DROPS_ENABLED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('add user notification when rebirth is enabled', () => {
|
||||||
|
user.stats.lvl = 51;
|
||||||
|
updateStats(user, { });
|
||||||
|
expect(user.addNotification).to.be.calledTwice; // once is for drops enabled
|
||||||
|
expect(user.addNotification).to.be.calledWith('REBIRTH_ENABLED');
|
||||||
|
});
|
||||||
|
|
||||||
context('assigns flags.levelDrops', () => {
|
context('assigns flags.levelDrops', () => {
|
||||||
it('for atom1', () => {
|
it('for atom1', () => {
|
||||||
user.stats.lvl = 16;
|
user.stats.lvl = 16;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ afterEach((done) => {
|
|||||||
export { sleep } from './sleep';
|
export { sleep } from './sleep';
|
||||||
|
|
||||||
export function generateUser (options = {}) {
|
export function generateUser (options = {}) {
|
||||||
return new User(options).toObject();
|
return new User(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateGroup (options = {}) {
|
export function generateGroup (options = {}) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ describe('Filters Controller', function() {
|
|||||||
scope = $rootScope.$new();
|
scope = $rootScope.$new();
|
||||||
// user.filters = {};
|
// user.filters = {};
|
||||||
User.setUser(user);
|
User.setUser(user);
|
||||||
|
User.user.filters = {};
|
||||||
userService = User;
|
userService = User;
|
||||||
$controller('FiltersCtrl', {$scope: scope, User: User});
|
$controller('FiltersCtrl', {$scope: scope, User: User});
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ describe('userServices', function() {
|
|||||||
it('saves user data to local storage', function(){
|
it('saves user data to local storage', function(){
|
||||||
user.save();
|
user.save();
|
||||||
var settings = JSON.parse(localStorage[STORAGE_SETTINGS_ID]);
|
var settings = JSON.parse(localStorage[STORAGE_SETTINGS_ID]);
|
||||||
var user_id = JSON.parse(localStorage[STORAGE_USER_ID]);
|
|
||||||
expect(settings).to.eql(user.settings);
|
expect(settings).to.eql(user.settings);
|
||||||
expect(user_id).to.eql(user.user);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
xit('alerts when not authenticated', function(){
|
xit('alerts when not authenticated', function(){
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
@import "./menu.styl"
|
@import "./menu.styl"
|
||||||
@import "./options.styl"
|
@import "./options.styl"
|
||||||
@import "./no-script.styl"
|
@import "./no-script.styl"
|
||||||
|
@import "./loading-screen.styl"
|
||||||
|
|
||||||
html,body,p,h1,ul,li,table,tr,th,td
|
html,body,p,h1,ul,li,table,tr,th,td
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|||||||
55
website/client/css/loading-screen.styl
Normal file
55
website/client/css/loading-screen.styl
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#loadingScreen
|
||||||
|
z-index: 9999
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
padding-top: 150px
|
||||||
|
|
||||||
|
@media (max-device-width: 768px), (orientation: landscape)
|
||||||
|
#loadingScreen
|
||||||
|
padding-top: 75px
|
||||||
|
|
||||||
|
#loadingScreen img
|
||||||
|
display: block
|
||||||
|
margin: 0 auto
|
||||||
|
width: 90%
|
||||||
|
|
||||||
|
#loadingScreen .loading-logo-icon
|
||||||
|
max-width: 87.5px
|
||||||
|
margin-bottom: 15px
|
||||||
|
|
||||||
|
#loadingScreen .loading-logo-text
|
||||||
|
max-width: 282.5px
|
||||||
|
|
||||||
|
.loading-spinner
|
||||||
|
margin: 100px auto 0
|
||||||
|
width: 105px
|
||||||
|
padding-left: 5px
|
||||||
|
text-align: center
|
||||||
|
margin-top: 20px
|
||||||
|
|
||||||
|
.loading-spinner > div
|
||||||
|
width: 16px
|
||||||
|
height: 16px
|
||||||
|
background-color: #432476
|
||||||
|
border-radius: 100%
|
||||||
|
display: inline-block
|
||||||
|
animation: sk-bouncedelay 1.7s infinite ease-in-out both
|
||||||
|
margin-right: 5px
|
||||||
|
|
||||||
|
.loading-spinner .spinner__item1
|
||||||
|
animation-delay: -0.60s
|
||||||
|
|
||||||
|
.loading-spinner .spinner__item2
|
||||||
|
animation-delay: -0.40s
|
||||||
|
|
||||||
|
.loading-spinner .spinner__item3
|
||||||
|
animation-delay: -0.20s
|
||||||
|
|
||||||
|
@keyframes sk-bouncedelay
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0)
|
||||||
|
opacity: 0
|
||||||
|
} 40% {
|
||||||
|
transform: scale(1.0)
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@ angular.module('habitrpg')
|
|||||||
// If it was, sync
|
// If it was, sync
|
||||||
function verifyUserUpdated (response) {
|
function verifyUserUpdated (response) {
|
||||||
var isApiCall = response.config.url.indexOf('api/v3') !== -1;
|
var isApiCall = response.config.url.indexOf('api/v3') !== -1;
|
||||||
var isUserAvailable = $rootScope.User && $rootScope.User.user && $rootScope.User.user._wrapped === true;
|
var isUserAvailable = $rootScope.appLoaded === true;
|
||||||
var hasUserV = response.data && response.data.userV;
|
var hasUserV = response.data && response.data.userV;
|
||||||
var isNotSync = response.config.url.indexOf('/api/v3/user') !== 0;
|
var isNotSync = response.config.url.indexOf('/api/v3/user') !== 0 || response.config.method !== 'GET';
|
||||||
|
|
||||||
if (isApiCall && isUserAvailable && hasUserV) {
|
if (isApiCall && isUserAvailable && hasUserV) {
|
||||||
var oldUserV = $rootScope.User.user._v;
|
var oldUserV = $rootScope.User.user._v;
|
||||||
@@ -25,6 +25,24 @@ angular.module('habitrpg')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyNewNotifications (response) {
|
||||||
|
// Ignore CRON notifications for manual syncs
|
||||||
|
var isUserLoaded = $rootScope.appLoaded === true;
|
||||||
|
|
||||||
|
if (response && response.data && response.data.notifications && response.data.notifications.length > 0) {
|
||||||
|
$rootScope.userNotifications = response.data.notifications.filter(function (notification) {
|
||||||
|
if (isUserLoaded && notification.type === 'CRON') {
|
||||||
|
// If the user is already loaded, do not show the notification, syncing will show it
|
||||||
|
// (the user will be synced automatically)
|
||||||
|
$rootScope.User.readNotification(notification.id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
request: function (config) {
|
request: function (config) {
|
||||||
var url = config.url;
|
var url = config.url;
|
||||||
@@ -43,6 +61,7 @@ angular.module('habitrpg')
|
|||||||
},
|
},
|
||||||
response: function(response) {
|
response: function(response) {
|
||||||
verifyUserUpdated(response);
|
verifyUserUpdated(response);
|
||||||
|
verifyNewNotifications(response);
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
responseError: function(response) {
|
responseError: function(response) {
|
||||||
@@ -5,6 +5,7 @@ habitrpg
|
|||||||
function($scope, $rootScope, Members, Shared, $http, Notification, Groups, Chat, $controller, Stats) {
|
function($scope, $rootScope, Members, Shared, $http, Notification, Groups, Chat, $controller, Stats) {
|
||||||
|
|
||||||
$controller('RootCtrl', {$scope: $scope});
|
$controller('RootCtrl', {$scope: $scope});
|
||||||
|
$rootScope.appLoaded = true;
|
||||||
|
|
||||||
$scope.timestamp = function(timestamp){
|
$scope.timestamp = function(timestamp){
|
||||||
return moment(timestamp).format($rootScope.User.user.preferences.dateFormat.toUpperCase());
|
return moment(timestamp).format($rootScope.User.user.preferences.dateFormat.toUpperCase());
|
||||||
|
|||||||
@@ -23,17 +23,6 @@ habitrpg.controller('NotificationCtrl',
|
|||||||
Notification.exp(after - before);
|
Notification.exp(after - before);
|
||||||
});
|
});
|
||||||
|
|
||||||
$rootScope.$watch('user.achievements', function(){
|
|
||||||
$rootScope.playSound('Achievement_Unlocked');
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
$rootScope.$watch('user.achievements.challenges.length', function(after, before) {
|
|
||||||
if (after === before) return;
|
|
||||||
if (after > before) {
|
|
||||||
$rootScope.openModal('wonChallenge', {controller: 'UserCtrl', size: 'sm'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$rootScope.$watch('user.stats.gp', function(after, before) {
|
$rootScope.$watch('user.stats.gp', function(after, before) {
|
||||||
if (after == before) return;
|
if (after == before) return;
|
||||||
if (User.user.stats.lvl == 0) return;
|
if (User.user.stats.lvl == 0) return;
|
||||||
@@ -82,35 +71,89 @@ habitrpg.controller('NotificationCtrl',
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$rootScope.$watch('user.achievements.streak', function(after, before){
|
// Avoid showing the same notiication more than once
|
||||||
if(before == undefined || after <= before) return;
|
var lastShownNotifications = [];
|
||||||
Notification.streak(User.user.achievements.streak);
|
|
||||||
$rootScope.playSound('Achievement_Unlocked');
|
function handleUserNotifications (after) {
|
||||||
if (!User.user.preferences.suppressModals.streak) {
|
if (!after || after.length === 0) return;
|
||||||
$rootScope.openModal('achievements/streak', {controller:'UserCtrl'});
|
|
||||||
}
|
after.forEach(function (notification) {
|
||||||
|
if (lastShownNotifications.indexOf(notification.id) !== -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastShownNotifications.push(notification.id);
|
||||||
|
if (lastShownNotifications.length > 10) {
|
||||||
|
lastShownNotifications.splice(0, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
var markAsRead = true;
|
||||||
|
|
||||||
|
switch (notification.type) {
|
||||||
|
case 'DROPS_ENABLED':
|
||||||
|
$rootScope.openModal('dropsEnabled');
|
||||||
|
break;
|
||||||
|
case 'REBIRTH_ENABLED':
|
||||||
|
$rootScope.openModal('rebirthEnabled');
|
||||||
|
break;
|
||||||
|
case 'WON_CHALLENGE':
|
||||||
|
$rootScope.openModal('wonChallenge', {controller: 'UserCtrl', size: 'sm'});
|
||||||
|
break;
|
||||||
|
case 'STREAK_ACHIEVEMENT':
|
||||||
|
Notification.streak(User.user.achievements.streak);
|
||||||
|
$rootScope.playSound('Achievement_Unlocked');
|
||||||
|
if (!User.user.preferences.suppressModals.streak) {
|
||||||
|
$rootScope.openModal('achievements/streak', {controller:'UserCtrl'});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ULTIMATE_GEAR_ACHIEVEMENT':
|
||||||
|
$rootScope.openModal('achievements/ultimateGear', {controller:'UserCtrl'});
|
||||||
|
break;
|
||||||
|
case 'REBIRTH_ACHIEVEMENT':
|
||||||
|
$rootScope.openModal('achievements/rebirth', {controller:'UserCtrl', size: 'sm'});
|
||||||
|
break;
|
||||||
|
case 'NEW_CONTRIBUTOR_LEVEL':
|
||||||
|
$rootScope.openModal('achievements/contributor',{controller:'UserCtrl'});
|
||||||
|
break;
|
||||||
|
case 'CRON':
|
||||||
|
if (notification.data) {
|
||||||
|
if (notification.data.hp) Notification.hp(notification.data.hp, 'hp');
|
||||||
|
if (notification.data.mp) Notification.mp(notification.data.mp);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
markAsRead = false; // If the notification is not implemented, skip it
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markAsRead) User.readNotification(notification.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
User.user.notifications = []; // reset the notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we don't use localStorage anymore, notifications for achievements and new contributor levels
|
||||||
|
// are now stored user.notifications.
|
||||||
|
$rootScope.$watchCollection('userNotifications', function (after) {
|
||||||
|
if (!User.user._wrapped) return;
|
||||||
|
handleUserNotifications(after);
|
||||||
});
|
});
|
||||||
|
|
||||||
$rootScope.$watch('user.achievements.ultimateGearSets', function(after, before){
|
var handleUserNotificationsOnFirstSync = _.once(function () {
|
||||||
if (_.isEqual(after,before) || !_.contains(User.user.achievements.ultimateGearSets, true)) return;
|
handleUserNotifications($rootScope.userNotifications);
|
||||||
$rootScope.openModal('achievements/ultimateGear', {controller:'UserCtrl'});
|
});
|
||||||
|
$rootScope.$on('userUpdated', handleUserNotificationsOnFirstSync);
|
||||||
|
|
||||||
|
// TODO what about this?
|
||||||
|
$rootScope.$watch('user.achievements', function(){
|
||||||
|
$rootScope.playSound('Achievement_Unlocked');
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
$rootScope.$watch('user.flags.armoireEmpty', function(after,before){
|
$rootScope.$watch('user.flags.armoireEmpty', function(after,before){
|
||||||
if (before == undefined || after == before || after == false) return;
|
if (after == before || after == false) return;
|
||||||
$rootScope.openModal('armoireEmpty');
|
$rootScope.openModal('armoireEmpty');
|
||||||
});
|
});
|
||||||
|
|
||||||
$rootScope.$watch('user.achievements.rebirths', function(after, before){
|
|
||||||
if(after === before) return;
|
|
||||||
$rootScope.openModal('achievements/rebirth', {controller:'UserCtrl', size: 'sm'});
|
|
||||||
});
|
|
||||||
|
|
||||||
$rootScope.$watch('user.contributor.level', function(after, before){
|
|
||||||
if (after === before || after < before || after == null) return;
|
|
||||||
$rootScope.openModal('achievements/contributor',{controller:'UserCtrl'});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Completed quest modal
|
// Completed quest modal
|
||||||
$scope.$watch('user.party.quest.completed', function(after, before){
|
$scope.$watch('user.party.quest.completed', function(after, before){
|
||||||
if (!after) return;
|
if (!after) return;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$rootScope.appLoaded = false; // also used to indicate when the user is fully loaded
|
||||||
$rootScope.TAVERN_ID = TAVERN_ID;
|
$rootScope.TAVERN_ID = TAVERN_ID;
|
||||||
$rootScope.User = User;
|
$rootScope.User = User;
|
||||||
$rootScope.user = user;
|
$rootScope.user = user;
|
||||||
@@ -39,7 +40,8 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
|
|||||||
$rootScope.Groups = Groups;
|
$rootScope.Groups = Groups;
|
||||||
$rootScope.toJson = angular.toJson;
|
$rootScope.toJson = angular.toJson;
|
||||||
$rootScope.Payments = Payments;
|
$rootScope.Payments = Payments;
|
||||||
|
$rootScope.userNotifications = [];
|
||||||
|
|
||||||
// Angular UI Router
|
// Angular UI Router
|
||||||
$rootScope.$state = $state;
|
$rootScope.$state = $state;
|
||||||
$rootScope.$stateParams = $stateParams;
|
$rootScope.$stateParams = $stateParams;
|
||||||
|
|||||||
@@ -289,16 +289,6 @@ function($rootScope, User, $timeout, $state, Analytics) {
|
|||||||
case 'options.inventory.equipment': return goto('equipment', 0);
|
case 'options.inventory.equipment': return goto('equipment', 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$rootScope.$watch('user.flags.dropsEnabled', function(after, before) {
|
|
||||||
if (alreadyShown(before,after)) return;
|
|
||||||
var eggs = User.user.items.eggs || {};
|
|
||||||
if (!eggs) eggs['Wolf'] = 1; // This is also set on the server
|
|
||||||
$rootScope.openModal('dropsEnabled');
|
|
||||||
});
|
|
||||||
$rootScope.$watch('user.flags.rebirthEnabled', function(after, before) {
|
|
||||||
if (alreadyShown(before, after)) return;
|
|
||||||
$rootScope.openModal('rebirthEnabled');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var Guide = {
|
var Guide = {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ angular.module('habitrpg')
|
|||||||
return $http({
|
return $http({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: url,
|
url: url,
|
||||||
|
ignoreLoadingBar: $rootScope.appLoaded !== true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
22
website/client/js/services/userNotificationsService.js
Normal file
22
website/client/js/services/userNotificationsService.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('habitrpg')
|
||||||
|
.factory('UserNotifications', ['$http',
|
||||||
|
function userNotificationsFactory($http) {
|
||||||
|
|
||||||
|
var lastRead; // keep track of last notification ID to avoid reding it twice
|
||||||
|
|
||||||
|
function readNotification (notificationId) {
|
||||||
|
if (lastRead === notificationId) return;
|
||||||
|
lastRead = notificationId;
|
||||||
|
|
||||||
|
return $http({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'api/v3/notifications/' + notificationId + '/read',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
readNotification: readNotification,
|
||||||
|
};
|
||||||
|
}]);
|
||||||
@@ -14,8 +14,8 @@ angular.module('habitrpg')
|
|||||||
/**
|
/**
|
||||||
* Services that persists and retrieves user from localStorage.
|
* Services that persists and retrieves user from localStorage.
|
||||||
*/
|
*/
|
||||||
.factory('User', ['$rootScope', '$http', '$location', '$window', 'STORAGE_USER_ID', 'STORAGE_SETTINGS_ID', 'Notification', 'ApiUrl', 'Tasks', 'Tags', 'Content',
|
.factory('User', ['$rootScope', '$http', '$location', '$window', 'STORAGE_USER_ID', 'STORAGE_SETTINGS_ID', 'Notification', 'ApiUrl', 'Tasks', 'Tags', 'Content', 'UserNotifications',
|
||||||
function($rootScope, $http, $location, $window, STORAGE_USER_ID, STORAGE_SETTINGS_ID, Notification, ApiUrl, Tasks, Tags, Content) {
|
function($rootScope, $http, $location, $window, STORAGE_USER_ID, STORAGE_SETTINGS_ID, Notification, ApiUrl, Tasks, Tags, Content, UserNotifications) {
|
||||||
var authenticated = false;
|
var authenticated = false;
|
||||||
var defaultSettings = {
|
var defaultSettings = {
|
||||||
auth: { apiId: '', apiToken: ''},
|
auth: { apiId: '', apiToken: ''},
|
||||||
@@ -38,11 +38,6 @@ angular.module('habitrpg')
|
|||||||
//first we populate user with schema
|
//first we populate user with schema
|
||||||
user.apiToken = user._id = ''; // we use id / apitoken to determine if registered
|
user.apiToken = user._id = ''; // we use id / apitoken to determine if registered
|
||||||
|
|
||||||
//than we try to load localStorage
|
|
||||||
if (localStorage.getItem(STORAGE_USER_ID)) {
|
|
||||||
_.extend(user, JSON.parse(localStorage.getItem(STORAGE_USER_ID)));
|
|
||||||
}
|
|
||||||
|
|
||||||
user._wrapped = false;
|
user._wrapped = false;
|
||||||
|
|
||||||
function syncUserTasks (tasks) {
|
function syncUserTasks (tasks) {
|
||||||
@@ -78,6 +73,7 @@ angular.module('habitrpg')
|
|||||||
return $http({
|
return $http({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: '/api/v3/user/',
|
url: '/api/v3/user/',
|
||||||
|
ignoreLoadingBar: $rootScope.appLoaded !== true,
|
||||||
})
|
})
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (response.data.message) Notification.text(response.data.message);
|
if (response.data.message) Notification.text(response.data.message);
|
||||||
@@ -108,14 +104,14 @@ angular.module('habitrpg')
|
|||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
var tasks = response.data.data;
|
var tasks = response.data.data;
|
||||||
syncUserTasks(tasks);
|
syncUserTasks(tasks);
|
||||||
save();
|
|
||||||
$rootScope.$emit('userSynced');
|
$rootScope.$emit('userSynced');
|
||||||
|
$rootScope.appLoaded = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var save = function () {
|
var save = function () {
|
||||||
localStorage.setItem(STORAGE_USER_ID, JSON.stringify(user));
|
|
||||||
localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(settings));
|
localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(settings));
|
||||||
|
localStorage.removeItem(STORAGE_USER_ID); // TODO remember to remove once it's been live for a few days
|
||||||
};
|
};
|
||||||
|
|
||||||
function callOpsFunctionAndRequest (opName, endPoint, method, paramString, opData) {
|
function callOpsFunctionAndRequest (opName, endPoint, method, paramString, opData) {
|
||||||
@@ -167,8 +163,6 @@ angular.module('habitrpg')
|
|||||||
var text = Content.gear.flat[openedItem.key].text();
|
var text = Content.gear.flat[openedItem.key].text();
|
||||||
Notification.drop(env.t('messageDropMysteryItem', {dropText: text}), openedItem);
|
Notification.drop(env.t('messageDropMysteryItem', {dropText: text}), openedItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
save();
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +208,6 @@ angular.module('habitrpg')
|
|||||||
} else {
|
} else {
|
||||||
user.ops.addTask(data);
|
user.ops.addTask(data);
|
||||||
}
|
}
|
||||||
save();
|
|
||||||
Tasks.createUserTasks(data.body);
|
Tasks.createUserTasks(data.body);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -225,7 +218,6 @@ angular.module('habitrpg')
|
|||||||
Notification.text(err.message);
|
Notification.text(err.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
save();
|
|
||||||
|
|
||||||
Tasks.scoreTask(data.params.task._id, data.params.direction).then(function (res) {
|
Tasks.scoreTask(data.params.task._id, data.params.direction).then(function (res) {
|
||||||
var tmp = res.data.data._tmp || {}; // used to notify drops, critical hits and other bonuses
|
var tmp = res.data.data._tmp || {}; // used to notify drops, critical hits and other bonuses
|
||||||
@@ -284,37 +276,35 @@ angular.module('habitrpg')
|
|||||||
|
|
||||||
sortTask: function (data) {
|
sortTask: function (data) {
|
||||||
user.ops.sortTask(data);
|
user.ops.sortTask(data);
|
||||||
save();
|
|
||||||
Tasks.moveTask(data.params.id, data.query.to);
|
Tasks.moveTask(data.params.id, data.query.to);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTask: function (task, data) {
|
updateTask: function (task, data) {
|
||||||
$window.habitrpgShared.ops.updateTask(task, data);
|
$window.habitrpgShared.ops.updateTask(task, data);
|
||||||
save();
|
|
||||||
Tasks.updateTask(task._id, data.body);
|
Tasks.updateTask(task._id, data.body);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTask: function (data) {
|
deleteTask: function (data) {
|
||||||
user.ops.deleteTask(data);
|
user.ops.deleteTask(data);
|
||||||
save();
|
|
||||||
Tasks.deleteTask(data.params.id);
|
Tasks.deleteTask(data.params.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearCompleted: function () {
|
clearCompleted: function () {
|
||||||
user.ops.clearCompleted(user.todos);
|
user.ops.clearCompleted(user.todos);
|
||||||
save();
|
|
||||||
Tasks.clearCompletedTodos();
|
Tasks.clearCompletedTodos();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
readNotification: function (notificationId) {
|
||||||
|
UserNotifications.readNotification(notificationId);
|
||||||
|
},
|
||||||
|
|
||||||
addTag: function(data) {
|
addTag: function(data) {
|
||||||
user.ops.addTag(data);
|
user.ops.addTag(data);
|
||||||
save();
|
|
||||||
Tags.createTag(data.body);
|
Tags.createTag(data.body);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTag: function(data) {
|
updateTag: function(data) {
|
||||||
user.ops.updateTag(data);
|
user.ops.updateTag(data);
|
||||||
save();
|
|
||||||
Tags.updateTag(data.params.id, data.body);
|
Tags.updateTag(data.params.id, data.body);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -326,7 +316,6 @@ angular.module('habitrpg')
|
|||||||
|
|
||||||
deleteTag: function(data) {
|
deleteTag: function(data) {
|
||||||
user.ops.deleteTag(data);
|
user.ops.deleteTag(data);
|
||||||
save();
|
|
||||||
Tags.deleteTag(data.params.id);
|
Tags.deleteTag(data.params.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -495,7 +484,6 @@ angular.module('habitrpg')
|
|||||||
data: updates,
|
data: updates,
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
save();
|
|
||||||
$rootScope.$emit('userSynced');
|
$rootScope.$emit('userSynced');
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,11 +38,11 @@
|
|||||||
"js/env.js",
|
"js/env.js",
|
||||||
|
|
||||||
"js/app.js",
|
"js/app.js",
|
||||||
"common/script/public/config.js",
|
"js/config.js",
|
||||||
|
|
||||||
"js/services/sharedServices.js",
|
"js/services/sharedServices.js",
|
||||||
"js/services/notificationServices.js",
|
"js/services/notificationServices.js",
|
||||||
"common/script/public/directives.js",
|
"js/directives/directives.js",
|
||||||
"js/services/analyticsServices.js",
|
"js/services/analyticsServices.js",
|
||||||
"js/services/groupServices.js",
|
"js/services/groupServices.js",
|
||||||
"js/services/chatServices.js",
|
"js/services/chatServices.js",
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"js/services/questServices.js",
|
"js/services/questServices.js",
|
||||||
"js/services/socialServices.js",
|
"js/services/socialServices.js",
|
||||||
"js/services/statServices.js",
|
"js/services/statServices.js",
|
||||||
|
"js/services/userNotificationsService.js",
|
||||||
"js/services/userServices.js",
|
"js/services/userServices.js",
|
||||||
"js/services/hallServices.js",
|
"js/services/hallServices.js",
|
||||||
|
|
||||||
@@ -131,6 +132,7 @@
|
|||||||
"js/static.js",
|
"js/static.js",
|
||||||
"js/services/analyticsServices.js",
|
"js/services/analyticsServices.js",
|
||||||
"js/services/notificationServices.js",
|
"js/services/notificationServices.js",
|
||||||
|
"js/services/userNotificationsService.js",
|
||||||
"js/services/userServices.js",
|
"js/services/userServices.js",
|
||||||
"js/services/sharedServices.js",
|
"js/services/sharedServices.js",
|
||||||
"js/services/socialServices.js",
|
"js/services/socialServices.js",
|
||||||
@@ -173,6 +175,7 @@
|
|||||||
"js/services/statServices.js",
|
"js/services/statServices.js",
|
||||||
"js/services/taskServices.js",
|
"js/services/taskServices.js",
|
||||||
"js/services/tagsServices.js",
|
"js/services/tagsServices.js",
|
||||||
|
"js/services/userNotificationsService.js",
|
||||||
"js/services/userServices.js",
|
"js/services/userServices.js",
|
||||||
"js/services/memberServices.js",
|
"js/services/memberServices.js",
|
||||||
"js/controllers/authCtrl.js",
|
"js/controllers/authCtrl.js",
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ api.updateHero = {
|
|||||||
tierDiff--;
|
tierDiff--;
|
||||||
newTier--; // give them gems for the next tier down if they weren't aready that tier
|
newTier--; // give them gems for the next tier down if they weren't aready that tier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hero.addNotification('NEW_CONTRIBUTOR_LEVEL');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateData.contributor) _.assign(hero.contributor, updateData.contributor);
|
if (updateData.contributor) _.assign(hero.contributor, updateData.contributor);
|
||||||
|
|||||||
47
website/server/controllers/api-v3/notifications.js
Normal file
47
website/server/controllers/api-v3/notifications.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { authWithHeaders } from '../../middlewares/api-v3/auth';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {
|
||||||
|
NotFound,
|
||||||
|
} from '../../libs/api-v3/errors';
|
||||||
|
|
||||||
|
let api = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiIgnore Not yet part of the public API
|
||||||
|
* @api {post} /api/v3/notifications/:notificationId/read Mark one notification as read
|
||||||
|
* @apiVersion 3.0.0
|
||||||
|
* @apiName ReadNotification
|
||||||
|
* @apiGroup Notification
|
||||||
|
*
|
||||||
|
* @apiParam {UUID} notificationId
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data user.notifications
|
||||||
|
*/
|
||||||
|
api.readNotification = {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/notifications/:notificationId/read',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
async handler (req, res) {
|
||||||
|
let user = res.locals.user;
|
||||||
|
|
||||||
|
req.checkParams('notificationId', res.t('notificationIdRequired')).notEmpty();
|
||||||
|
|
||||||
|
let validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
let index = _.findIndex(user.notifications, {
|
||||||
|
id: req.params.notificationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
throw new NotFound(res.t('messageNotificationNotFound'));
|
||||||
|
}
|
||||||
|
|
||||||
|
user.notifications.splice(index, 1);
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.respond(200, user.notifications);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
@@ -39,7 +39,7 @@ export function getBuildUrl (url) {
|
|||||||
return `/${buildFiles[url] || url}`;
|
return `/${buildFiles[url] || url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getManifestFiles (page) {
|
export function getManifestFiles (page, type) {
|
||||||
let files = manifestFiles[page];
|
let files = manifestFiles[page];
|
||||||
|
|
||||||
if (!files) throw new Error(`Page "${page}" not found!`);
|
if (!files) throw new Error(`Page "${page}" not found!`);
|
||||||
@@ -47,15 +47,25 @@ export function getManifestFiles (page) {
|
|||||||
let htmlCode = '';
|
let htmlCode = '';
|
||||||
|
|
||||||
if (IS_PROD) {
|
if (IS_PROD) {
|
||||||
htmlCode += `<link rel="stylesheet" type="text/css" href="${getBuildUrl(page + '.css')}">`; // eslint-disable-line prefer-template
|
if (type !== 'js') {
|
||||||
htmlCode += `<script type="text/javascript" src="${getBuildUrl(page + '.js')}"></script>`; // eslint-disable-line prefer-template
|
htmlCode += `<link rel="stylesheet" type="text/css" href="${getBuildUrl(page + '.css')}">`; // eslint-disable-line prefer-template
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type !== 'css') {
|
||||||
|
htmlCode += `<script type="text/javascript" src="${getBuildUrl(page + '.js')}"></script>`; // eslint-disable-line prefer-template
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
files.css.forEach((file) => {
|
if (type !== 'js') {
|
||||||
htmlCode += `<link rel="stylesheet" type="text/css" href="${getBuildUrl(file)}">`;
|
files.css.forEach((file) => {
|
||||||
});
|
htmlCode += `<link rel="stylesheet" type="text/css" href="${getBuildUrl(file)}">`;
|
||||||
files.js.forEach((file) => {
|
});
|
||||||
htmlCode += `<script type="text/javascript" src="${getBuildUrl(file)}"></script>`;
|
}
|
||||||
});
|
|
||||||
|
if (type !== 'css') {
|
||||||
|
files.js.forEach((file) => {
|
||||||
|
htmlCode += `<script type="text/javascript" src="${getBuildUrl(file)}"></script>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return htmlCode;
|
return htmlCode;
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ function performSleepTasks (user, tasksByType, now) {
|
|||||||
export function cron (options = {}) {
|
export function cron (options = {}) {
|
||||||
let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options;
|
let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options;
|
||||||
|
|
||||||
|
// Record pre-cron values of HP and MP to show notifications later
|
||||||
|
let beforeCronStats = _.pick(user.stats, ['hp', 'mp']);
|
||||||
|
|
||||||
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
||||||
// User is only allowed a certain number of drops a day. This resets the count.
|
// User is only allowed a certain number of drops a day. This resets the count.
|
||||||
if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0;
|
if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0;
|
||||||
@@ -279,6 +282,25 @@ export function cron (options = {}) {
|
|||||||
let _progress = _.cloneDeep(progress);
|
let _progress = _.cloneDeep(progress);
|
||||||
_.merge(progress, {down: 0, up: 0, collectedItems: 0});
|
_.merge(progress, {down: 0, up: 0, collectedItems: 0});
|
||||||
|
|
||||||
|
// Send notification for changes in HP and MP
|
||||||
|
|
||||||
|
// First remove a possible previous cron notification
|
||||||
|
// we don't want to flood the users with many cron notifications at once
|
||||||
|
|
||||||
|
let oldCronNotif = user.notifications.toObject().find((notif, index) => {
|
||||||
|
if (notif.type === 'CRON') {
|
||||||
|
user.notifications.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
user.addNotification('CRON', {
|
||||||
|
hp: user.stats.hp - beforeCronStats.hp - (oldCronNotif ? oldCronNotif.data.hp : 0),
|
||||||
|
mp: user.stats.mp - beforeCronStats.mp - (oldCronNotif ? oldCronNotif.data.mp : 0),
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: Clean PMs - keep 200 for subscribers and 50 for free users. Should also be done while resting in the inn
|
// TODO: Clean PMs - keep 200 for subscribers and 50 for free users. Should also be done while resting in the inn
|
||||||
// let numberOfPMs = Object.keys(user.inbox.messages).length;
|
// let numberOfPMs = Object.keys(user.inbox.messages).length;
|
||||||
// if (numberOfPMs > maxPMs) {
|
// if (numberOfPMs > maxPMs) {
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ module.exports = function responseHandler (req, res, next) {
|
|||||||
// sends back the current user._v in the response so that the client
|
// sends back the current user._v in the response so that the client
|
||||||
// can verify if it's the most up to date data.
|
// can verify if it's the most up to date data.
|
||||||
// Considered part of the private API for now and not officially supported
|
// Considered part of the private API for now and not officially supported
|
||||||
if (user && req.query.userV) {
|
if (user) {
|
||||||
response.userV = user._v;
|
response.notifications = user.notifications.map(notification => notification.toJSON());
|
||||||
|
if (req.query.userV) {
|
||||||
|
response.userV = user._v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(status).json(response);
|
res.status(status).json(response);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ module.exports = function staticMiddleware (expressApp) {
|
|||||||
expressApp.use(express.static(BUILD_DIR, { maxAge: MAX_AGE }));
|
expressApp.use(express.static(BUILD_DIR, { maxAge: MAX_AGE }));
|
||||||
expressApp.use('/common/dist', express.static(`${PUBLIC_DIR}/../../common/dist`, { maxAge: MAX_AGE }));
|
expressApp.use('/common/dist', express.static(`${PUBLIC_DIR}/../../common/dist`, { maxAge: MAX_AGE }));
|
||||||
expressApp.use('/common/audio', express.static(`${PUBLIC_DIR}/../../common/audio`, { maxAge: MAX_AGE }));
|
expressApp.use('/common/audio', express.static(`${PUBLIC_DIR}/../../common/audio`, { maxAge: MAX_AGE }));
|
||||||
expressApp.use('/common/script/public', express.static(`${PUBLIC_DIR}/../../common/script/public`, { maxAge: MAX_AGE }));
|
|
||||||
expressApp.use('/common/img', express.static(`${PUBLIC_DIR}/../../common/img`, { maxAge: MAX_AGE }));
|
expressApp.use('/common/img', express.static(`${PUBLIC_DIR}/../../common/img`, { maxAge: MAX_AGE }));
|
||||||
expressApp.use(express.static(PUBLIC_DIR));
|
expressApp.use(express.static(PUBLIC_DIR));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { sendTxn as txnEmail } from '../libs/api-v3/email';
|
|||||||
import sendPushNotification from '../libs/api-v3/pushNotifications';
|
import sendPushNotification from '../libs/api-v3/pushNotifications';
|
||||||
import cwait from 'cwait';
|
import cwait from 'cwait';
|
||||||
|
|
||||||
let Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
let schema = new Schema({
|
let schema = new Schema({
|
||||||
name: {type: String, required: true},
|
name: {type: String, required: true},
|
||||||
@@ -286,7 +286,11 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
|
|||||||
if (winner) {
|
if (winner) {
|
||||||
winner.achievements.challenges.push(challenge.name);
|
winner.achievements.challenges.push(challenge.name);
|
||||||
winner.balance += challenge.prize / 4;
|
winner.balance += challenge.prize / 4;
|
||||||
|
|
||||||
|
winner.addNotification('WON_CHALLENGE');
|
||||||
|
|
||||||
let savedWinner = await winner.save();
|
let savedWinner = await winner.save();
|
||||||
|
|
||||||
if (savedWinner.preferences.emailNotifications.wonChallenge !== false) {
|
if (savedWinner.preferences.emailNotifications.wonChallenge !== false) {
|
||||||
txnEmail(savedWinner, 'won-challenge', [
|
txnEmail(savedWinner, 'won-challenge', [
|
||||||
{name: 'CHALLENGE_NAME', content: challenge.name},
|
{name: 'CHALLENGE_NAME', content: challenge.name},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import baseModel from '../libs/api-v3/baseModel';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
|
|
||||||
let Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
export let schema = new Schema({
|
export let schema = new Schema({
|
||||||
id: {
|
id: {
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import baseModel from '../libs/api-v3/baseModel';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { preenHistory } from '../libs/api-v3/preening';
|
import { preenHistory } from '../libs/api-v3/preening';
|
||||||
|
|
||||||
let Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
let discriminatorOptions = {
|
let discriminatorOptions = {
|
||||||
discriminatorKey: 'type', // the key that distinguishes task types
|
discriminatorKey: 'type', // the key that distinguishes task types
|
||||||
};
|
};
|
||||||
|
|||||||
170
website/server/models/user/hooks.js
Normal file
170
website/server/models/user/hooks.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import shared from '../../../../common';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
|
import * as Tasks from '../task';
|
||||||
|
import Bluebird from 'bluebird';
|
||||||
|
import baseModel from '../../libs/api-v3/baseModel';
|
||||||
|
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
|
schema.plugin(baseModel, {
|
||||||
|
// noSet is not used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...)
|
||||||
|
noSet: [],
|
||||||
|
private: ['auth.local.hashed_password', 'auth.local.salt', '_cronSignature'],
|
||||||
|
toJSONTransform: function userToJSON (plainObj, originalDoc) {
|
||||||
|
plainObj._tmp = originalDoc._tmp; // be sure to send down drop notifs
|
||||||
|
|
||||||
|
return plainObj;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
schema.post('init', function postInitUser (doc) {
|
||||||
|
shared.wrap(doc);
|
||||||
|
});
|
||||||
|
|
||||||
|
function _populateDefaultTasks (user, taskTypes) {
|
||||||
|
let tagsI = taskTypes.indexOf('tag');
|
||||||
|
|
||||||
|
if (tagsI !== -1) {
|
||||||
|
user.tags = _.map(shared.content.userDefaults.tags, (tag) => {
|
||||||
|
let newTag = _.cloneDeep(tag);
|
||||||
|
|
||||||
|
// tasks automatically get _id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here
|
||||||
|
newTag.id = shared.uuid();
|
||||||
|
// Render tag's name in user's language
|
||||||
|
newTag.name = newTag.name(user.preferences.language);
|
||||||
|
return newTag;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tasksToCreate = [];
|
||||||
|
|
||||||
|
if (tagsI !== -1) {
|
||||||
|
taskTypes = _.clone(taskTypes);
|
||||||
|
taskTypes.splice(tagsI, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(taskTypes, (taskType) => {
|
||||||
|
let tasksOfType = _.map(shared.content.userDefaults[`${taskType}s`], (taskDefaults) => {
|
||||||
|
let newTask = new Tasks[taskType](taskDefaults);
|
||||||
|
|
||||||
|
newTask.userId = user._id;
|
||||||
|
newTask.text = taskDefaults.text(user.preferences.language);
|
||||||
|
if (newTask.notes) newTask.notes = taskDefaults.notes(user.preferences.language);
|
||||||
|
if (taskDefaults.checklist) {
|
||||||
|
newTask.checklist = _.map(taskDefaults.checklist, (checklistItem) => {
|
||||||
|
checklistItem.text = checklistItem.text(user.preferences.language);
|
||||||
|
return checklistItem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTask.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
tasksToCreate.push(...tasksOfType);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Bluebird.all(tasksToCreate)
|
||||||
|
.then((tasksCreated) => {
|
||||||
|
_.each(tasksCreated, (task) => {
|
||||||
|
user.tasksOrder[`${task.type}s`].push(task._id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _populateDefaultsForNewUser (user) {
|
||||||
|
let taskTypes;
|
||||||
|
let iterableFlags = user.flags.toObject();
|
||||||
|
|
||||||
|
if (user.registeredThrough === 'habitica-web' || user.registeredThrough === 'habitica-android') {
|
||||||
|
taskTypes = ['habit', 'daily', 'todo', 'reward', 'tag'];
|
||||||
|
|
||||||
|
_.each(iterableFlags.tutorial.common, (val, section) => {
|
||||||
|
user.flags.tutorial.common[section] = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
taskTypes = ['todo', 'tag'];
|
||||||
|
user.flags.showTour = false;
|
||||||
|
|
||||||
|
_.each(iterableFlags.tour, (val, section) => {
|
||||||
|
user.flags.tour[section] = -2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return _populateDefaultTasks(user, taskTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setProfileName (user) {
|
||||||
|
let fb = user.auth.facebook;
|
||||||
|
|
||||||
|
let localUsername = user.auth.local && user.auth.local.username;
|
||||||
|
let facebookUsername = fb && (fb.displayName || fb.name || fb.username || `${fb.first_name && fb.first_name} ${fb.last_name}`);
|
||||||
|
let anonymous = 'Anonymous';
|
||||||
|
|
||||||
|
return localUsername || facebookUsername || anonymous;
|
||||||
|
}
|
||||||
|
|
||||||
|
schema.pre('save', true, function preSaveUser (next, done) {
|
||||||
|
next();
|
||||||
|
|
||||||
|
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
|
||||||
|
this.preferences.dayStart = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.profile.name) {
|
||||||
|
this.profile.name = _setProfileName(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if Beast Master should be awarded
|
||||||
|
let beastMasterProgress = shared.count.beastMasterProgress(this.items.pets);
|
||||||
|
|
||||||
|
if (beastMasterProgress >= 90 || this.achievements.beastMasterCount > 0) {
|
||||||
|
this.achievements.beastMaster = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if Mount Master should be awarded
|
||||||
|
let mountMasterProgress = shared.count.mountMasterProgress(this.items.mounts);
|
||||||
|
|
||||||
|
if (mountMasterProgress >= 90 || this.achievements.mountMasterCount > 0) {
|
||||||
|
this.achievements.mountMaster = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if Triad Bingo should be awarded
|
||||||
|
|
||||||
|
let dropPetCount = shared.count.dropPetsCurrentlyOwned(this.items.pets);
|
||||||
|
let qualifiesForTriad = dropPetCount >= 90 && mountMasterProgress >= 90;
|
||||||
|
|
||||||
|
if (qualifiesForTriad || this.achievements.triadBingoCount > 0) {
|
||||||
|
this.achievements.triadBingo = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable weekly recap emails for old users who sign in
|
||||||
|
if (this.flags.lastWeeklyRecapDiscriminator) {
|
||||||
|
// Enable weekly recap emails in 24 hours
|
||||||
|
this.flags.lastWeeklyRecap = moment().subtract(6, 'days').toDate();
|
||||||
|
// Unset the field so this is run only once
|
||||||
|
this.flags.lastWeeklyRecapDiscriminator = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXAMPLE CODE for allowing all existing and new players to be
|
||||||
|
// automatically granted an item during a certain time period:
|
||||||
|
// if (!this.items.pets['JackOLantern-Base'] && moment().isBefore('2014-11-01'))
|
||||||
|
// this.items.pets['JackOLantern-Base'] = 5;
|
||||||
|
|
||||||
|
// our own version incrementer
|
||||||
|
if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0;
|
||||||
|
this._v++;
|
||||||
|
|
||||||
|
// Populate new users with default content
|
||||||
|
if (this.isNew) {
|
||||||
|
_populateDefaultsForNewUser(this)
|
||||||
|
.then(() => done())
|
||||||
|
.catch(done);
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
schema.pre('update', function preUpdateUser () {
|
||||||
|
this.update({}, {$inc: {_v: 1}});
|
||||||
|
});
|
||||||
33
website/server/models/user/index.js
Normal file
33
website/server/models/user/index.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
|
require('./hooks');
|
||||||
|
require('./methods');
|
||||||
|
|
||||||
|
// A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private)
|
||||||
|
export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt
|
||||||
|
preferences.chair preferences.costume preferences.sleep preferences.background profile stats
|
||||||
|
achievements party backer contributor auth.timestamps items`;
|
||||||
|
|
||||||
|
// The minimum amount of data needed when populating multiple users
|
||||||
|
export let nameFields = 'profile.name';
|
||||||
|
|
||||||
|
export { schema };
|
||||||
|
|
||||||
|
export let model = mongoose.model('User', schema);
|
||||||
|
|
||||||
|
// Initially export an empty object so external requires will get
|
||||||
|
// the right object by reference when it's defined later
|
||||||
|
// Otherwise it would remain undefined if requested before the query executes
|
||||||
|
export let mods = [];
|
||||||
|
|
||||||
|
mongoose.model('User')
|
||||||
|
.find({'contributor.admin': true})
|
||||||
|
.sort('-contributor.level -backer.npc profile.name')
|
||||||
|
.select('profile contributor backer')
|
||||||
|
.exec()
|
||||||
|
.then((foundMods) => {
|
||||||
|
// Using push to maintain the reference to mods
|
||||||
|
mods.push(...foundMods);
|
||||||
|
});
|
||||||
129
website/server/models/user/methods.js
Normal file
129
website/server/models/user/methods.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import shared from '../../../../common';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import * as Tasks from '../task';
|
||||||
|
import Bluebird from 'bluebird';
|
||||||
|
import {
|
||||||
|
chatDefaults,
|
||||||
|
TAVERN_ID,
|
||||||
|
} from '../group';
|
||||||
|
import { defaults } from 'lodash';
|
||||||
|
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
|
schema.methods.isSubscribed = function isSubscribed () {
|
||||||
|
return !!this.purchased.plan.customerId; // eslint-disable-line no-implicit-coercion
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get an array of groups ids the user is member of
|
||||||
|
schema.methods.getGroups = function getUserGroups () {
|
||||||
|
let userGroups = this.guilds.slice(0); // clone user.guilds so we don't modify the original
|
||||||
|
if (this.party._id) userGroups.push(this.party._id);
|
||||||
|
userGroups.push(TAVERN_ID);
|
||||||
|
return userGroups;
|
||||||
|
};
|
||||||
|
|
||||||
|
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, message) {
|
||||||
|
let sender = this;
|
||||||
|
|
||||||
|
shared.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
|
||||||
|
userToReceiveMessage.inbox.newMessages++;
|
||||||
|
userToReceiveMessage._v++;
|
||||||
|
userToReceiveMessage.markModified('inbox.messages');
|
||||||
|
|
||||||
|
shared.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage)));
|
||||||
|
sender.markModified('inbox.messages');
|
||||||
|
|
||||||
|
let promises = [userToReceiveMessage.save(), sender.save()];
|
||||||
|
await Bluebird.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
schema.methods.addNotification = function addUserNotification (type, data = {}) {
|
||||||
|
this.notifications.push({
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the user model)
|
||||||
|
// These will be removed once API v2 is discontinued
|
||||||
|
|
||||||
|
// Get all the tasks belonging to a user,
|
||||||
|
schema.methods.getTasks = function getUserTasks () {
|
||||||
|
let args = Array.from(arguments);
|
||||||
|
let cb;
|
||||||
|
let type;
|
||||||
|
|
||||||
|
if (args.length === 1) {
|
||||||
|
cb = args[0];
|
||||||
|
} else {
|
||||||
|
type = args[0];
|
||||||
|
cb = args[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = {
|
||||||
|
userId: this._id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type) query.type = type;
|
||||||
|
|
||||||
|
Tasks.Task.find(query, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Given user and an array of tasks, return an API compatible user + tasks obj
|
||||||
|
schema.methods.addTasksToUser = function addTasksToUser (tasks) {
|
||||||
|
let obj = this.toJSON();
|
||||||
|
|
||||||
|
obj.id = obj._id;
|
||||||
|
obj.filters = {};
|
||||||
|
|
||||||
|
obj.tags = obj.tags.map(tag => {
|
||||||
|
return {
|
||||||
|
id: tag.id,
|
||||||
|
name: tag.name,
|
||||||
|
challenge: tag.challenge,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let tasksOrder = obj.tasksOrder; // Saving a reference because we won't return it
|
||||||
|
|
||||||
|
obj.habits = [];
|
||||||
|
obj.dailys = [];
|
||||||
|
obj.todos = [];
|
||||||
|
obj.rewards = [];
|
||||||
|
|
||||||
|
obj.tasksOrder = undefined;
|
||||||
|
let unordered = [];
|
||||||
|
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
// We want to push the task at the same position where it's stored in tasksOrder
|
||||||
|
let pos = tasksOrder[`${task.type}s`].indexOf(task._id);
|
||||||
|
if (pos === -1) { // Should never happen, it means the lists got out of sync
|
||||||
|
unordered.push(task.toJSONV2());
|
||||||
|
} else {
|
||||||
|
obj[`${task.type}s`][pos] = task.toJSONV2();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reconcile unordered items
|
||||||
|
unordered.forEach((task) => {
|
||||||
|
obj[`${task.type}s`].push(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove null values that can be created when inserting tasks at an index > length
|
||||||
|
['habits', 'dailys', 'rewards', 'todos'].forEach((type) => {
|
||||||
|
obj[type] = _.compact(obj[type]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return the data maintaining backward compatibility
|
||||||
|
schema.methods.getTransformedData = function getTransformedData (cb) {
|
||||||
|
let self = this;
|
||||||
|
this.getTasks((err, tasks) => {
|
||||||
|
if (err) return cb(err);
|
||||||
|
cb(null, self.addTasksToUser(tasks));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// END of API v2 methods
|
||||||
@@ -1,22 +1,16 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import shared from '../../../common';
|
import shared from '../../../../common';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import moment from 'moment';
|
import { schema as TagSchema } from '../tag';
|
||||||
import * as Tasks from './task';
|
|
||||||
import Bluebird from 'bluebird';
|
|
||||||
import { schema as TagSchema } from './tag';
|
|
||||||
import baseModel from '../libs/api-v3/baseModel';
|
|
||||||
import {
|
import {
|
||||||
chatDefaults,
|
schema as UserNotificationSchema,
|
||||||
TAVERN_ID,
|
} from '../userNotification';
|
||||||
} from './group';
|
|
||||||
import { defaults } from 'lodash';
|
|
||||||
|
|
||||||
let Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
// User schema definition
|
// User schema definition
|
||||||
export let schema = new Schema({
|
let schema = new Schema({
|
||||||
apiToken: {
|
apiToken: {
|
||||||
type: String,
|
type: String,
|
||||||
default: shared.uuid,
|
default: shared.uuid,
|
||||||
@@ -495,6 +489,7 @@ export let schema = new Schema({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
notifications: [UserNotificationSchema],
|
||||||
tags: [TagSchema],
|
tags: [TagSchema],
|
||||||
|
|
||||||
inbox: {
|
inbox: {
|
||||||
@@ -514,312 +509,13 @@ export let schema = new Schema({
|
|||||||
extra: {type: Schema.Types.Mixed, default: () => {
|
extra: {type: Schema.Types.Mixed, default: () => {
|
||||||
return {};
|
return {};
|
||||||
}},
|
}},
|
||||||
pushDevices: {
|
pushDevices: [{
|
||||||
type: [{
|
regId: {type: String},
|
||||||
regId: {type: String},
|
type: {type: String},
|
||||||
type: {type: String},
|
}],
|
||||||
}],
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
strict: true,
|
strict: true,
|
||||||
minimize: false, // So empty objects are returned
|
minimize: false, // So empty objects are returned
|
||||||
});
|
});
|
||||||
|
|
||||||
schema.plugin(baseModel, {
|
module.exports = schema;
|
||||||
// noSet is not used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...)
|
|
||||||
noSet: [],
|
|
||||||
private: ['auth.local.hashed_password', 'auth.local.salt', '_cronSignature'],
|
|
||||||
toJSONTransform: function userToJSON (plainObj, originalDoc) {
|
|
||||||
// plainObj.filters = {}; // TODO Not saved, remove?
|
|
||||||
plainObj._tmp = originalDoc._tmp; // be sure to send down drop notifs
|
|
||||||
|
|
||||||
return plainObj;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private)
|
|
||||||
export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt
|
|
||||||
preferences.chair preferences.costume preferences.sleep preferences.background profile stats
|
|
||||||
achievements party backer contributor auth.timestamps items`;
|
|
||||||
|
|
||||||
// The minimum amount of data needed when populating multiple users
|
|
||||||
export let nameFields = 'profile.name';
|
|
||||||
|
|
||||||
schema.post('init', function postInitUser (doc) {
|
|
||||||
shared.wrap(doc);
|
|
||||||
});
|
|
||||||
|
|
||||||
function _populateDefaultTasks (user, taskTypes) {
|
|
||||||
let tagsI = taskTypes.indexOf('tag');
|
|
||||||
|
|
||||||
if (tagsI !== -1) {
|
|
||||||
user.tags = _.map(shared.content.userDefaults.tags, (tag) => {
|
|
||||||
let newTag = _.cloneDeep(tag);
|
|
||||||
|
|
||||||
// tasks automatically get _id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here
|
|
||||||
newTag.id = shared.uuid();
|
|
||||||
// Render tag's name in user's language
|
|
||||||
newTag.name = newTag.name(user.preferences.language);
|
|
||||||
return newTag;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let tasksToCreate = [];
|
|
||||||
|
|
||||||
if (tagsI !== -1) {
|
|
||||||
taskTypes = _.clone(taskTypes);
|
|
||||||
taskTypes.splice(tagsI, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
_.each(taskTypes, (taskType) => {
|
|
||||||
let tasksOfType = _.map(shared.content.userDefaults[`${taskType}s`], (taskDefaults) => {
|
|
||||||
let newTask = new Tasks[taskType](taskDefaults);
|
|
||||||
|
|
||||||
newTask.userId = user._id;
|
|
||||||
newTask.text = taskDefaults.text(user.preferences.language);
|
|
||||||
if (newTask.notes) newTask.notes = taskDefaults.notes(user.preferences.language);
|
|
||||||
if (taskDefaults.checklist) {
|
|
||||||
newTask.checklist = _.map(taskDefaults.checklist, (checklistItem) => {
|
|
||||||
checklistItem.text = checklistItem.text(user.preferences.language);
|
|
||||||
return checklistItem;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTask.save();
|
|
||||||
});
|
|
||||||
|
|
||||||
tasksToCreate.push(...tasksOfType);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Bluebird.all(tasksToCreate)
|
|
||||||
.then((tasksCreated) => {
|
|
||||||
_.each(tasksCreated, (task) => {
|
|
||||||
user.tasksOrder[`${task.type}s`].push(task._id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _populateDefaultsForNewUser (user) {
|
|
||||||
let taskTypes;
|
|
||||||
let iterableFlags = user.flags.toObject();
|
|
||||||
|
|
||||||
if (user.registeredThrough === 'habitica-web' || user.registeredThrough === 'habitica-android') {
|
|
||||||
taskTypes = ['habit', 'daily', 'todo', 'reward', 'tag'];
|
|
||||||
|
|
||||||
_.each(iterableFlags.tutorial.common, (val, section) => {
|
|
||||||
user.flags.tutorial.common[section] = true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
taskTypes = ['todo', 'tag'];
|
|
||||||
user.flags.showTour = false;
|
|
||||||
|
|
||||||
_.each(iterableFlags.tour, (val, section) => {
|
|
||||||
user.flags.tour[section] = -2;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return _populateDefaultTasks(user, taskTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _setProfileName (user) {
|
|
||||||
let fb = user.auth.facebook;
|
|
||||||
|
|
||||||
let localUsername = user.auth.local && user.auth.local.username;
|
|
||||||
let facebookUsername = fb && (fb.displayName || fb.name || fb.username || `${fb.first_name && fb.first_name} ${fb.last_name}`);
|
|
||||||
let anonymous = 'Anonymous';
|
|
||||||
|
|
||||||
return localUsername || facebookUsername || anonymous;
|
|
||||||
}
|
|
||||||
|
|
||||||
schema.pre('save', true, function preSaveUser (next, done) {
|
|
||||||
next();
|
|
||||||
|
|
||||||
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
|
|
||||||
this.preferences.dayStart = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.profile.name) {
|
|
||||||
this.profile.name = _setProfileName(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if Beast Master should be awarded
|
|
||||||
let beastMasterProgress = shared.count.beastMasterProgress(this.items.pets);
|
|
||||||
|
|
||||||
if (beastMasterProgress >= 90 || this.achievements.beastMasterCount > 0) {
|
|
||||||
this.achievements.beastMaster = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if Mount Master should be awarded
|
|
||||||
let mountMasterProgress = shared.count.mountMasterProgress(this.items.mounts);
|
|
||||||
|
|
||||||
if (mountMasterProgress >= 90 || this.achievements.mountMasterCount > 0) {
|
|
||||||
this.achievements.mountMaster = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if Triad Bingo should be awarded
|
|
||||||
|
|
||||||
let dropPetCount = shared.count.dropPetsCurrentlyOwned(this.items.pets);
|
|
||||||
let qualifiesForTriad = dropPetCount >= 90 && mountMasterProgress >= 90;
|
|
||||||
|
|
||||||
if (qualifiesForTriad || this.achievements.triadBingoCount > 0) {
|
|
||||||
this.achievements.triadBingo = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable weekly recap emails for old users who sign in
|
|
||||||
if (this.flags.lastWeeklyRecapDiscriminator) {
|
|
||||||
// Enable weekly recap emails in 24 hours
|
|
||||||
this.flags.lastWeeklyRecap = moment().subtract(6, 'days').toDate();
|
|
||||||
// Unset the field so this is run only once
|
|
||||||
this.flags.lastWeeklyRecapDiscriminator = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXAMPLE CODE for allowing all existing and new players to be
|
|
||||||
// automatically granted an item during a certain time period:
|
|
||||||
// if (!this.items.pets['JackOLantern-Base'] && moment().isBefore('2014-11-01'))
|
|
||||||
// this.items.pets['JackOLantern-Base'] = 5;
|
|
||||||
|
|
||||||
// our own version incrementer
|
|
||||||
if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0;
|
|
||||||
this._v++;
|
|
||||||
|
|
||||||
// Populate new users with default content
|
|
||||||
if (this.isNew) {
|
|
||||||
_populateDefaultsForNewUser(this)
|
|
||||||
.then(() => done())
|
|
||||||
.catch(done);
|
|
||||||
} else {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
schema.pre('update', function preUpdateUser () {
|
|
||||||
this.update({}, {$inc: {_v: 1}});
|
|
||||||
});
|
|
||||||
|
|
||||||
schema.methods.isSubscribed = function isSubscribed () {
|
|
||||||
return !!this.purchased.plan.customerId; // eslint-disable-line no-implicit-coercion
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get an array of groups ids the user is member of
|
|
||||||
schema.methods.getGroups = function getUserGroups () {
|
|
||||||
let userGroups = this.guilds.slice(0); // clone user.guilds so we don't modify the original
|
|
||||||
if (this.party._id) userGroups.push(this.party._id);
|
|
||||||
userGroups.push(TAVERN_ID);
|
|
||||||
return userGroups;
|
|
||||||
};
|
|
||||||
|
|
||||||
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, message) {
|
|
||||||
let sender = this;
|
|
||||||
|
|
||||||
shared.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
|
|
||||||
userToReceiveMessage.inbox.newMessages++;
|
|
||||||
userToReceiveMessage._v++;
|
|
||||||
userToReceiveMessage.markModified('inbox.messages');
|
|
||||||
|
|
||||||
shared.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage)));
|
|
||||||
sender.markModified('inbox.messages');
|
|
||||||
|
|
||||||
let promises = [userToReceiveMessage.save(), sender.save()];
|
|
||||||
await Bluebird.all(promises);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the user model)
|
|
||||||
// These will be removed once API v2 is discontinued
|
|
||||||
|
|
||||||
// Get all the tasks belonging to a user,
|
|
||||||
schema.methods.getTasks = function getUserTasks () {
|
|
||||||
let args = Array.from(arguments);
|
|
||||||
let cb;
|
|
||||||
let type;
|
|
||||||
|
|
||||||
if (args.length === 1) {
|
|
||||||
cb = args[0];
|
|
||||||
} else {
|
|
||||||
type = args[0];
|
|
||||||
cb = args[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = {
|
|
||||||
userId: this._id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type) query.type = type;
|
|
||||||
|
|
||||||
Tasks.Task.find(query, cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Given user and an array of tasks, return an API compatible user + tasks obj
|
|
||||||
schema.methods.addTasksToUser = function addTasksToUser (tasks) {
|
|
||||||
let obj = this.toJSON();
|
|
||||||
|
|
||||||
obj.id = obj._id;
|
|
||||||
obj.filters = {};
|
|
||||||
|
|
||||||
obj.tags = obj.tags.map(tag => {
|
|
||||||
return {
|
|
||||||
id: tag.id,
|
|
||||||
name: tag.name,
|
|
||||||
challenge: tag.challenge,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let tasksOrder = obj.tasksOrder; // Saving a reference because we won't return it
|
|
||||||
|
|
||||||
obj.habits = [];
|
|
||||||
obj.dailys = [];
|
|
||||||
obj.todos = [];
|
|
||||||
obj.rewards = [];
|
|
||||||
|
|
||||||
obj.tasksOrder = undefined;
|
|
||||||
let unordered = [];
|
|
||||||
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
// We want to push the task at the same position where it's stored in tasksOrder
|
|
||||||
let pos = tasksOrder[`${task.type}s`].indexOf(task._id);
|
|
||||||
if (pos === -1) { // Should never happen, it means the lists got out of sync
|
|
||||||
unordered.push(task.toJSONV2());
|
|
||||||
} else {
|
|
||||||
obj[`${task.type}s`][pos] = task.toJSONV2();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reconcile unordered items
|
|
||||||
unordered.forEach((task) => {
|
|
||||||
obj[`${task.type}s`].push(task);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove null values that can be created when inserting tasks at an index > length
|
|
||||||
['habits', 'dailys', 'rewards', 'todos'].forEach((type) => {
|
|
||||||
obj[type] = _.compact(obj[type]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Return the data maintaining backward compatibility
|
|
||||||
schema.methods.getTransformedData = function getTransformedData (cb) {
|
|
||||||
let self = this;
|
|
||||||
this.getTasks((err, tasks) => {
|
|
||||||
if (err) return cb(err);
|
|
||||||
cb(null, self.addTasksToUser(tasks));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// END of API v2 methods
|
|
||||||
export let model = mongoose.model('User', schema);
|
|
||||||
|
|
||||||
// Initially export an empty object so external requires will get
|
|
||||||
// the right object by reference when it's defined later
|
|
||||||
// Otherwise it would remain undefined if requested before the query executes
|
|
||||||
export let mods = [];
|
|
||||||
|
|
||||||
mongoose.model('User')
|
|
||||||
.find({'contributor.admin': true})
|
|
||||||
.sort('-contributor.level -backer.npc profile.name')
|
|
||||||
.select('profile contributor backer')
|
|
||||||
.exec()
|
|
||||||
.then((foundMods) => {
|
|
||||||
// Using push to maintain the reference to mods
|
|
||||||
mods.push(...foundMods);
|
|
||||||
}); // In case of failure we don't want this to crash the whole server
|
|
||||||
42
website/server/models/userNotification.js
Normal file
42
website/server/models/userNotification.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import baseModel from '../libs/api-v3/baseModel';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import validator from 'validator';
|
||||||
|
|
||||||
|
const NOTIFICATION_TYPES = [
|
||||||
|
'DROPS_ENABLED',
|
||||||
|
'REBIRTH_ENABLED',
|
||||||
|
'WON_CHALLENGE',
|
||||||
|
'STREAK_ACHIEVEMENT',
|
||||||
|
'ULTIMATE_GEAR_ACHIEVEMENT',
|
||||||
|
'REBIRTH_ACHIEVEMENT',
|
||||||
|
'NEW_CONTRIBUTOR_LEVEL',
|
||||||
|
'CRON',
|
||||||
|
];
|
||||||
|
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
export let schema = new Schema({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: uuid,
|
||||||
|
validate: [validator.isUUID, 'Invalid uuid.'],
|
||||||
|
},
|
||||||
|
type: {type: String, required: true, enum: NOTIFICATION_TYPES},
|
||||||
|
data: {type: Schema.Types.Mixed, default: () => {
|
||||||
|
return {};
|
||||||
|
}},
|
||||||
|
}, {
|
||||||
|
strict: true,
|
||||||
|
minimize: false, // So empty objects are returned
|
||||||
|
_id: false, // use id instead of _id
|
||||||
|
});
|
||||||
|
|
||||||
|
schema.plugin(baseModel, {
|
||||||
|
noSet: ['_id', 'id'],
|
||||||
|
timestamps: true,
|
||||||
|
private: ['updatedAt'],
|
||||||
|
_id: false, // use id instead of _id
|
||||||
|
});
|
||||||
|
|
||||||
|
export let model = mongoose.model('UserNotification', schema);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
doctype html
|
doctype html
|
||||||
//html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":applyingAction}', ui-keypress="{27:'castCancel()'}")
|
//html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":applyingAction}', ui-keypress="{27:'castCancel()'}")
|
||||||
html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":applyingAction}', ui-keyup="{27:'castCancel()'}")
|
html(ng-app='habitrpg', ng-controller='RootCtrl', ng-class='{"applying-action":applyingAction}', ui-keyup="{27:'castCancel()'}")
|
||||||
head
|
head
|
||||||
title(ng-bind="env.t('habitica') + ' | ' + $root.pageTitle")
|
title(ng-bind="env.t('habitica') + ' | ' + $root.pageTitle")
|
||||||
// ?v=1 needed to force refresh
|
// ?v=1 needed to force refresh
|
||||||
@@ -13,34 +13,50 @@ html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":a
|
|||||||
meta(name='apple-mobile-web-app-capable', content='yes')
|
meta(name='apple-mobile-web-app-capable', content='yes')
|
||||||
meta(name='mobile-web-app-capable', content='yes')
|
meta(name='mobile-web-app-capable', content='yes')
|
||||||
|
|
||||||
include ./shared/new-relic
|
!= env.getManifestFiles("app", "css")
|
||||||
//FIXME for some reason this won't load when in footerCtrl.js#deferredScripts()
|
|
||||||
script(type="text/javascript", src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4", async="async")
|
|
||||||
|
|
||||||
script(type='text/javascript').
|
// Does not work correctly inside .css file
|
||||||
window.env = !{JSON.stringify(env._.pick(env, env.clientVars))};
|
style(type='text/css').
|
||||||
|
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
|
||||||
!= env.getManifestFiles("app")
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
//webfonts
|
//webfonts
|
||||||
link(href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic,700italic', rel='stylesheet', type='text/css')
|
link(href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic,700italic', rel='stylesheet', type='text/css')
|
||||||
|
body
|
||||||
|
#loadingScreen(ng-if='!appLoaded')
|
||||||
|
// Loading screen images are inlined to avoid lag while loading (~5-6KB)
|
||||||
|
img.loading-logo-icon(src='data:image/png;base64,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')
|
||||||
|
img.loading-logo-text(src='data:image/png;base64,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')
|
||||||
|
.loading-spinner
|
||||||
|
.spinner__item1
|
||||||
|
.spinner__item2
|
||||||
|
.spinner__item3
|
||||||
|
.spinner__item4
|
||||||
|
.ng-cloak(ng-if='appLoaded', ng-controller='GroupsCtrl')
|
||||||
|
include ./shared/mixins
|
||||||
|
include ./shared/avatar/index
|
||||||
|
include ./shared/header/menu
|
||||||
|
include ./shared/modals/index
|
||||||
|
include ./shared/header/header
|
||||||
|
include ./shared/tasks/index
|
||||||
|
include ./main/index
|
||||||
|
include ./options/index
|
||||||
|
|
||||||
|
#notification-area(ng-controller='NotificationCtrl')
|
||||||
|
#wrap.container-fluid
|
||||||
|
.row
|
||||||
|
.col-md-12.exp-chart(ng-show='charts.exp')
|
||||||
|
#main(ui-view)
|
||||||
|
|
||||||
body(ng-cloak, ng-controller='GroupsCtrl')
|
include ./shared/footer
|
||||||
include ./shared/noscript
|
include ./shared/noscript
|
||||||
include ./shared/mixins
|
|
||||||
include ./shared/avatar/index
|
|
||||||
include ./shared/header/menu
|
|
||||||
include ./shared/modals/index
|
|
||||||
include ./shared/header/header
|
|
||||||
include ./shared/tasks/index
|
|
||||||
include ./main/index
|
|
||||||
include ./options/index
|
|
||||||
|
|
||||||
#notification-area(ng-controller='NotificationCtrl')
|
// Load javascript at the end
|
||||||
#wrap.container-fluid
|
script(type='text/javascript').
|
||||||
.row
|
window.env = !{JSON.stringify(env._.pick(env, env.clientVars))};
|
||||||
.col-md-12.exp-chart(ng-show='charts.exp')
|
|
||||||
#main(ui-view)
|
|
||||||
|
|
||||||
include ./shared/footer
|
!= env.getManifestFiles('app', 'js')
|
||||||
|
|
||||||
|
// TODO for some reason this won't load when in footerCtrl.js#deferredScripts()
|
||||||
|
script(type='text/javascript', src='//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4', async='async')
|
||||||
|
|||||||
Reference in New Issue
Block a user