Compare commits

...

19 Commits

Author SHA1 Message Date
Sabe Jones
0a840ca952 4.93.4 2019-05-03 08:47:40 -05:00
HydeHunter2
c0837e3b3c Fix challenge update (#11148)
* Fix challenge update

Fix some problem in challenge update

* Fix test

* Move leader to noUpdate

* Move leader to noUpdate
2019-05-03 15:35:56 +02:00
Sabe Jones
d8ea3bd23a 4.93.3 2019-05-02 14:25:07 -05:00
Sabe Jones
0ce11b82df Merge branch 'develop' into release 2019-05-02 14:25:02 -05:00
Sabe Jones
b4c47b4afd chore(sprites): compile 2019-05-02 14:24:39 -05:00
Sabe Jones
4777850601 chore(event): end Flinging 2019-05-02 14:24:29 -05:00
Sabe Jones
9a3e208c9b Merge branch 'release' into develop 2019-04-30 14:17:32 -05:00
Sabe Jones
792d5998b0 4.93.2 2019-04-30 14:17:14 -05:00
Sabe Jones
53515fd3f4 chore(news): Bailey 2019-04-30 14:17:05 -05:00
Sabe Jones
1135ab946e Sabrecat/groups quick wins (#11146)
* WIP(groups): quickish wins

* WIP(groups): two quick wins
1. Don't show task creation button if user is not leader or manager
2. Don't require JS confirm() for approving tasks

* fix(group-plans): allow delete from options button

* fix(group-plans): update tasksOrder when task deleted

* fix(group-tasks): dismiss notification when user takes action

* refactor(tasks): DRY out create button styling

* fix(group-tasks): sync after claiming/unclaiming

* fix(claiming): better sync and notif handling

* fix(tasks): force sync instead of explicitly clearing notif

* fix(tasks): reposition task creation button

* fix(group-tasks): default to single completion

* fix(group-tasks): move completion condition field above approval switch

* fix(group-tasks): todo validation error and approval notif dismissal

* fix(group-tasks): default single completion on client

* fix(group-tasks): move completion condition up more

* fix(group-tasks): maintain client-side user assignment list

* fix(group-tasks): remove approval notifications when task deleted

* fix(group-tasks): send assigned task to top of task list

* fix(group-tasks): remove useless tag filter dropdown

* feat(group-tasks): notify user of assigned task

* fix(group-tasks): don't allow approval of tasks w/ no approval request

* fix(tests): adjust expectations

* fix(group-tasks): more sensible action on assignment notif click
2019-04-29 13:38:28 -05:00
HydeHunter2
5a15c73fca Reload after rebirth (#11125)
* Add restart after rebirth

Page will be reloaded after purchasing "Orb of Rebirth"

* Remove restart after closing achievement

This reload is not needed, as the page now reloads immediately after purchasing "Orb of Rebirth"

* Move rebirth notification to modal

* Delete references to rebirth notification
2019-04-27 19:32:33 +02:00
HydeHunter2
3f99c14a37 Seasonal alert removed for items you own (#11135) 2019-04-27 19:28:35 +02:00
HydeHunter2
9e515d96c3 Remove reward button for non-leader/non-admin (#11136) 2019-04-27 19:26:02 +02:00
HydeHunter2
40e0017b17 Separate tags of different types (#11123)
Challenge, group and user tags are separated
2019-04-27 19:22:09 +02:00
HydeHunter2
251563690e Fix tag text overlapping (#11124)
* Fix word-wrapping in user

* Fix word-wrapping in taskModal
2019-04-27 19:21:05 +02:00
negue
83070e211d Inbox: Add API to list conversations (#11110)
* Add API to list inbox conversations

* fix test + add api doc

* use `.lean()`

* orderBy after the the grouped conversations are loaded

* fix ordering
2019-04-26 18:45:05 +02:00
Matteo Pagliazzi
3be075ad43 fix(tests): Items Utils > castItemVal fix fn call 2019-04-26 00:02:53 +02:00
Matteo Pagliazzi
043e0fb819 fix #9514: await user update client sid 2019-04-25 22:49:58 +02:00
Matteo Pagliazzi
9d473cc92e wip: fix setting (some) items values in the hall of heroes (#11133) 2019-04-25 22:41:43 +02:00
88 changed files with 7414 additions and 7205 deletions

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "habitica",
"version": "4.93.1",
"version": "4.93.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.93.1",
"version": "4.93.4",
"main": "./website/server/index.js",
"dependencies": {
"@google-cloud/trace-agent": "^3.6.0",

View File

@@ -2,6 +2,7 @@
import {
validateItemPath,
getDefaultOwnedGear,
castItemVal,
} from '../../../../../website/server/libs/items/utils';
describe('Items Utils', () => {
@@ -64,4 +65,49 @@ describe('Items Utils', () => {
expect(validateItemPath('items.quests.invalid')).to.equal(false);
});
});
describe('castItemVal', () => {
it('returns the item val untouched if not an item path', () => {
expect(castItemVal('notitems.gear.owned.item', 'a string')).to.equal('a string');
});
it('returns the item val untouched if an unsupported path', () => {
expect(castItemVal('items.gear.equipped.weapon', 'a string')).to.equal('a string');
expect(castItemVal('items.currentPet', 'a string')).to.equal('a string');
expect(castItemVal('items.special.snowball', 'a string')).to.equal('a string');
});
it('converts values for pets paths to numbers', () => {
expect(castItemVal('items.pets.Wolf-CottonCandyPink', '5')).to.equal(5);
expect(castItemVal('items.pets.Wolf-Invalid', '5')).to.equal(5);
});
it('converts values for eggs paths to numbers', () => {
expect(castItemVal('items.eggs.LionCub', '5')).to.equal(5);
expect(castItemVal('items.eggs.Armadillo', '5')).to.equal(5);
expect(castItemVal('items.eggs.NotAnArmadillo', '5')).to.equal(5);
});
it('converts values for hatching potions paths to numbers', () => {
expect(castItemVal('items.hatchingPotions.Base', '5')).to.equal(5);
expect(castItemVal('items.hatchingPotions.StarryNight', '5')).to.equal(5);
expect(castItemVal('items.hatchingPotions.Invalid', '5')).to.equal(5);
});
it('converts values for food paths to numbers', () => {
expect(castItemVal('items.food.Cake_Base', '5')).to.equal(5);
expect(castItemVal('items.food.Cake_Invalid', '5')).to.equal(5);
});
it('converts values for mounts paths to numbers', () => {
expect(castItemVal('items.mounts.Cactus-Base', '5')).to.equal(5);
expect(castItemVal('items.mounts.Aether-Invisible', '5')).to.equal(5);
expect(castItemVal('items.mounts.Aether-Invalid', '5')).to.equal(5);
});
it('converts values for quests paths to numbers', () => {
expect(castItemVal('items.quests.atom3', '5')).to.equal(5);
expect(castItemVal('items.quests.invalid', '5')).to.equal(5);
});
});
});

View File

@@ -56,11 +56,11 @@ describe('PUT /challenges/:challengeId', () => {
tasksOrder: 'new order',
official: true,
shortName: 'new short name',
leader: member._id,
// applied
name: 'New Challenge Name',
description: 'New challenge description.',
leader: member._id,
});
expect(res.prize).to.equal(0);
@@ -76,12 +76,12 @@ describe('PUT /challenges/:challengeId', () => {
expect(res.shortName).not.to.equal('new short name');
expect(res.leader).to.eql({
_id: member._id,
id: member._id,
profile: {name: member.profile.name},
_id: user._id,
id: user._id,
profile: {name: user.profile.name},
auth: {
local: {
username: member.auth.local.username,
username: user.auth.local.username,
},
},
flags: {

View File

@@ -60,4 +60,10 @@ describe('GET /inbox/messages', () => {
expect(messages.length).to.equal(4);
});
it('returns only the messages of one conversation', async () => {
const messages = await user.get(`/inbox/messages?conversation=${otherUser.id}`);
expect(messages.length).to.equal(3);
});
});

View File

@@ -63,6 +63,38 @@ describe('Groups DELETE /tasks/:id', () => {
});
});
it('removes deleted taskʾs approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await user.put(`/tasks/${task._id}/`, {
requiresApproval: true,
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
await member2.del(`/tasks/${task._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(member2.notifications.length).to.equal(1);
});
it('unlinks assigned user', async () => {
await user.del(`/tasks/${task._id}`);

View File

@@ -53,18 +53,29 @@ describe('POST /tasks/:id/approve/:userId', () => {
it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(2);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications.length).to.equal(3);
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
@@ -77,18 +88,28 @@ describe('POST /tasks/:id/approve/:userId', () => {
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(2);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications.length).to.equal(3);
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
@@ -132,6 +153,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
@@ -141,6 +172,17 @@ describe('POST /tasks/:id/approve/:userId', () => {
});
});
it('prevents approving a task if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
it('completes master task when single-completion task is approved', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
@@ -151,6 +193,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
@@ -172,6 +224,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let member2Tasks = await member2.get('/tasks/user');
@@ -193,6 +255,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}`);
@@ -214,6 +286,25 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let member2Tasks = await member2.get('/tasks/user');
let member2SyncedTask = find(member2Tasks, findAssignedTask);
await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);

View File

@@ -51,7 +51,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
});
});
it('marks as task as needing more work', async () => {
it('marks a task as needing more work', async () => {
const initialNotifications = member.notifications.length;
await user.post(`/tasks/${task._id}/assign/${member._id}`);
@@ -77,7 +77,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
// Check that the notification is correct
expect(member.notifications.length).to.equal(initialNotifications + 1);
expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -131,7 +131,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
expect(member.notifications.length).to.equal(initialNotifications + 1);
expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -167,6 +167,17 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({

View File

@@ -129,6 +129,13 @@ describe('POST /tasks/:id/score/:direction', () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`);

View File

@@ -113,6 +113,17 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
expect(syncedTask).to.exist;
});
it('sends a notification to assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await member.sync();
let groupTask = await user.get(`/tasks/group/${guild._id}`);
expect(member.notifications.length).to.equal(1);
expect(member.notifications[0].type).to.equal('GROUP_TASK_ASSIGNED');
expect(member.notifications[0].taskId).to.equal(groupTask._id);
});
it('assigns a task to multiple users', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`);

View File

@@ -86,6 +86,13 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(syncedTask).to.not.exist;
});
it('removes task assignment notification from unassigned user', async () => {
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(0);
});
it('unassigns a user and only that user from a task', async () => {
await user.post(`/tasks/${task._id}/assign/${member2._id}`);

View File

@@ -0,0 +1,44 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
describe('GET /inbox/conversations', () => {
let user;
let otherUser;
let thirdUser;
before(async () => {
[user, otherUser, thirdUser] = await Promise.all([generateUser(), generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await user.post('/members/send-private-message', {
toUserId: thirdUser.id,
message: 'third',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
});
// message to yourself
await user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fifth',
});
});
it('returns the conversations', async () => {
const result = await user.get('/inbox/conversations');
expect(result.length).to.be.equal(3);
expect(result[0].user).to.be.equal(user.profile.name);
expect(result[0].username).to.be.equal(user.auth.local.username);
});
});

View File

@@ -1,72 +1,30 @@
.promo_april_fools_2019 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -840px;
width: 423px;
height: 147px;
}
.promo_armoire_backgrounds_201904 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -424px -840px;
background-position: 0px -277px;
width: 423px;
height: 147px;
}
.promo_butterflies {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 676px;
height: 676px;
}
.promo_celestial_rainbow_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -433px -677px;
width: 423px;
height: 147px;
}
.promo_classes_spring2019 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -677px;
width: 432px;
height: 162px;
}
.promo_egg_hunt {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px 0px;
width: 354px;
height: 147px;
}
.promo_mystery_201904 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px -444px;
background-position: 0px -425px;
width: 282px;
height: 147px;
}
.promo_seasonalshop_spring {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px -592px;
width: 162px;
height: 138px;
}
.promo_shiny_seeds {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px -296px;
width: 351px;
height: 147px;
}
.promo_spring_avatar_customizations {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px -148px;
width: 354px;
height: 147px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1168px -592px;
background-position: -424px -277px;
width: 96px;
height: 69px;
}
.scene_spells {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -328px 0px;
width: 312px;
height: 222px;
}
.scene_yesterdailies_repeatables {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -677px 0px;
background-position: 0px 0px;
width: 327px;
height: 276px;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -2,8 +2,8 @@
// possible values are: normal, fall, habitoween, thanksgiving, winter, nye, birthday, valentines, spring, summer
// more to be added on future seasons
$npc_market_flavor: 'spring';
$npc_quests_flavor: 'spring';
$npc_seasonal_flavor: 'spring';
$npc_timetravelers_flavor: 'spring';
$npc_tavern_flavor: 'spring';
$npc_market_flavor: 'normal';
$npc_quests_flavor: 'normal';
$npc_seasonal_flavor: 'normal';
$npc_timetravelers_flavor: 'normal';
$npc_tavern_flavor: 'normal';

View File

@@ -164,30 +164,30 @@ export default {
classGear (heroClass) {
if (heroClass === 'rogue') {
return {
armor: 'armor_special_spring2019Rogue',
head: 'head_special_spring2019Rogue',
shield: 'shield_special_spring2019Rogue',
weapon: 'weapon_special_spring2019Rogue',
armor: 'armor_rogue_5',
head: 'head_rogue_5',
shield: 'shield_rogue_6',
weapon: 'weapon_rogue_6',
};
} else if (heroClass === 'wizard') {
return {
armor: 'armor_special_spring2019Mage',
head: 'head_special_spring2019Mage',
weapon: 'weapon_special_spring2019Mage',
armor: 'armor_wizard_5',
head: 'head_wizard_5',
weapon: 'weapon_wizard_6',
};
} else if (heroClass === 'healer') {
return {
armor: 'armor_special_spring2019Healer',
head: 'head_special_spring2019Healer',
shield: 'shield_special_spring2019Healer',
weapon: 'weapon_special_spring2019Healer',
armor: 'armor_healer_5',
head: 'head_healer_5',
shield: 'shield_healer_5',
weapon: 'weapon_healer_6',
};
} else {
return {
armor: 'armor_special_spring2019Warrior',
head: 'head_special_spring2019Warrior',
shield: 'shield_special_spring2019Warrior',
weapon: 'weapon_special_spring2019Warrior',
armor: 'armor_warrior_5',
head: 'head_warrior_5',
shield: 'shield_warrior_5',
weapon: 'weapon_warrior_6',
};
}
},

View File

@@ -1,5 +1,5 @@
<template lang="pug">
b-modal#rebirth(:title="$t('modalAchievement')", size='md', :hide-footer="true", @hidden="reloadPage()")
b-modal#rebirth(:title="$t('modalAchievement')", size='md', :hide-footer="true")
.modal-body
.col-12
// @TODO: +achievementAvatar('sun',0)
@@ -41,9 +41,6 @@
close () {
this.$root.$emit('bv::hide::modal', 'rebirth');
},
reloadPage () {
window.location.reload(true);
},
},
};
</script>

View File

@@ -376,6 +376,8 @@ export default {
openMemberProgressModal (member) {
this.$root.$emit('habitica:challenge:member-progress', {
progressMemberId: member._id,
isLeader: this.isLeader,
isAdmin: this.isAdmin,
});
},
async exportChallengeCsv () {

View File

@@ -1,6 +1,6 @@
<template lang="pug">
b-modal#challenge-member-modal(title="User Progress", size='lg')
.row.award-row
.row.award-row(v-if='isLeader || isAdmin')
.col-12.text-center
button.btn.btn-primary(v-once, @click='closeChallenge()') {{ $t('awardWinners') }}
.row
@@ -37,12 +37,16 @@ export default {
reward: [],
},
memberId: '',
isLeader: false,
isAdmin: false,
};
},
mounted () {
this.$root.$on('habitica:challenge:member-progress', (data) => {
if (!data.progressMemberId) return;
this.memberId = data.progressMemberId;
this.isLeader = data.isLeader;
this.isAdmin = data.isAdmin;
this.$root.$emit('bv::show::modal', 'challenge-member-modal');
});
},

View File

@@ -16,54 +16,7 @@
h1 {{ $t('groupTasksTitle') }}
// @TODO: Abstract to component!
.col-12.col-md-4
.input-group
input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText")
.filter-panel(v-if="isFilterPanelOpen")
.tags-category(v-for="tagsType in tagsByType", v-if="tagsType.tags.length > 0", :key="tagsType.key")
.tags-header.col-12
strong(v-once) {{ $t(tagsType.key) }}
a.d-block(v-if="tagsType.key === 'tags' && !editingTags", @click="editTags()") {{ $t('editTags2') }}
.tags-list.container.col-12
.row(:class="{'no-gutters': !editingTags}")
template(v-if="editingTags && tagsType.key === 'tags'")
.col-12.col-md-6(v-for="(tag, tagIndex) in tagsSnap")
.inline-edit-input-group.tag-edit-item.input-group
input.tag-edit-input.inline-edit-input.form-control(type="text", :value="tag.name")
.input-group-append(@click="removeTag(tagIndex)")
.svg-icon.destroy-icon(v-html="icons.destroy")
.col-12.col-md-6
input.new-tag-item.edit-tag-item.inline-edit-input.form-control(type="text", :placeholder="$t('newTag')", @keydown.enter="addTag($event)", v-model="newTag")
template(v-else)
.col-12.col-md-6(v-for="(tag, tagIndex) in tagsType.tags")
.custom-control.custom-checkbox
input.custom-control-input(
type="checkbox",
:checked="isTagSelected(tag)",
@change="toggleTag(tag)",
:id="`tag-${tagsType.key}-${tagIndex}`",
)
label.custom-control-label(:for="`tag-${tagsType.key}-${tagIndex}`") {{ tag.name }}
.filter-panel-footer.clearfix
template(v-if="editingTags === true")
.text-center
a.mr-3.btn-filters-primary(@click="saveTags()", v-once) {{ $t('saveEdits') }}
a.btn-filters-secondary(@click="cancelTagsEditing()", v-once) {{ $t('cancel') }}
template(v-else)
.float-left
a.btn-filters-danger(@click="resetFilters()", v-once) {{ $t('resetFilters') }}
.float-right
a.mr-3.btn-filters-primary(@click="applyFilters()", v-once) {{ $t('applyFilters') }}
a.btn-filters-secondary(@click="closeFilterPanel()", v-once) {{ $t('cancel') }}
span.input-group-append
button.btn.btn-secondary.filter-button(
type="button",
@click="toggleFilterPanel()",
:class="{'filter-button-open': selectedTags.length > 0}",
)
.d-flex.align-items-center
span(v-once) {{ $t('filter') }}
.svg-icon.filter-icon(v-html="icons.filter")
input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText")
.create-task-area.d-flex(v-if='canCreateTasks')
transition(name="slide-tasks-btns")
.d-flex(v-if="openCreateBtn")
@@ -99,10 +52,6 @@
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/create-task.scss';
.user-tasks-page {
padding-top: 31px;
}
.tasks-navigation {
margin-bottom: 40px;
}
@@ -114,133 +63,6 @@
margin-right: 8px;
padding-top: 6px;
}
.dropdown-icon-item .svg-icon {
width: 16px;
}
button.btn.btn-secondary.filter-button {
box-shadow: none;
border-radius: 2px;
border: 1px solid $gray-400 !important;
&:hover, &:active, &:focus, &.open {
box-shadow: none;
border-color: $purple-500 !important;
color: $gray-50 !important;
}
&.filter-button-open {
color: $purple-200 !important;
.filter-icon {
color: $purple-200 !important;
}
}
.filter-icon {
height: 10px;
width: 12px;
color: $gray-50;
margin-left: 15px;
}
}
.filter-panel {
position: absolute;
padding-left: 24px;
padding-right: 24px;
max-width: 40vw;
width: 100%;
z-index: 9999;
background: $white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
top: 44px;
left: 20vw;
font-size: 14px;
line-height: 1.43;
text-overflow: ellipsis;
.tags-category {
border-bottom: 1px solid $gray-600;
padding-bottom: 24px;
padding-top: 24px;
}
.tags-header {
flex-basis: 96px;
flex-shrink: 0;
a {
font-size: 12px;
line-height: 1.33;
color: $blue-10;
margin-top: 4px;
&:focus, &:hover, &:active {
text-decoration: underline;
}
}
}
.tag-edit-input {
border-bottom: 1px solid $gray-500 !important;
&:focus, &:focus ~ .input-group-append {
border-color: $purple-500 !important;
}
}
.new-tag-item {
width: 100%;
background-repeat: no-repeat;
background-position: center left 10px;
border-bottom: 1px solid $gray-500 !important;
background-size: 10px 10px;
padding-left: 40px;
background-image: url(~client/assets/svg/for-css/positive.svg);
}
.tag-edit-item .input-group-append {
border-bottom: 1px solid $gray-500 !important;
&:focus {
border-color: $purple-500;
}
}
.custom-control-label {
margin-left: 10px;
}
.filter-panel-footer {
padding-top: 16px;
padding-bottom: 16px;
a {
&:focus, &:hover, &:active {
text-decoration: underline;
}
}
.btn-filters-danger {
color: $red-50;
}
.btn-filters-primary {
color: $blue-10;
}
.btn-filters-secondary {
color: $gray-300;
}
}
}
.button-label {
display: inline-block;
}
</style>
<script>

View File

@@ -0,0 +1,26 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
:read-after-click="true",
@click="action",
)
div(slot="content", v-html="notification.data.message")
</template>
<script>
import BaseNotification from './base';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
methods: {
action () {
this.$router.push({ name: 'tasks'});
},
},
};
</script>

View File

@@ -87,6 +87,7 @@ import CHALLENGE_INVITATION from './notifications/challengeInvitation';
import QUEST_INVITATION from './notifications/questInvitation';
import GROUP_TASK_APPROVAL from './notifications/groupTaskApproval';
import GROUP_TASK_APPROVED from './notifications/groupTaskApproved';
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
import NEW_MYSTERY_ITEMS from './notifications/newMysteryItems';
import CARD_RECEIVED from './notifications/cardReceived';
@@ -102,7 +103,7 @@ export default {
// One component for each type
NEW_STUFF, GROUP_TASK_NEEDS_WORK,
GUILD_INVITATION, PARTY_INVITATION, CHALLENGE_INVITATION,
QUEST_INVITATION, GROUP_TASK_APPROVAL, GROUP_TASK_APPROVED,
QUEST_INVITATION, GROUP_TASK_APPROVAL, GROUP_TASK_APPROVED, GROUP_TASK_ASSIGNED,
UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED,
NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE,
WorldBoss: WORLD_BOSS,
@@ -118,7 +119,7 @@ export default {
openStatus: undefined,
actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK',
'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK', 'GROUP_TASK_APPROVAL',
],
// A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel.
@@ -126,7 +127,7 @@ export default {
handledNotifications: [
'NEW_STUFF', 'GROUP_TASK_NEEDS_WORK',
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
'VERIFY_USERNAME',

View File

@@ -132,11 +132,6 @@ const NOTIFICATIONS = {
label: ($t) => `${$t('achievement')}: ${$t('gearAchievementNotification')}`,
modalId: 'ultimate-gear',
},
REBIRTH_ACHIEVEMENT: {
label: ($t) => `${$t('achievement')}: ${$t('rebirthBegan')}`,
achievement: true,
modalId: 'rebirth',
},
GUILD_JOINED_ACHIEVEMENT: {
label: ($t) => `${$t('achievement')}: ${$t('joinedGuild')}`,
achievement: true,
@@ -360,18 +355,7 @@ export default {
this.playSound(config.sound);
}
if (type === 'REBIRTH_ACHIEVEMENT') {
// reload if the user hasn't clicked on the notification
const timeOut = setTimeout(() => {
window.location.reload(true);
}, 60000);
this.text(config.label(this.$t), () => {
// prevent the current reload timeout
clearTimeout(timeOut);
this.$root.$emit('bv::show::modal', config.modalId);
}, false);
} else if (forceToModal) {
if (forceToModal) {
this.$root.$emit('bv::show::modal', config.modalId);
} else {
this.text(config.label(this.$t), () => {
@@ -573,8 +557,11 @@ export default {
}, this.user.preferences.suppressModals.streak);
this.playSound('Achievement_Unlocked');
break;
case 'ULTIMATE_GEAR_ACHIEVEMENT':
case 'REBIRTH_ACHIEVEMENT':
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
break;
case 'ULTIMATE_GEAR_ACHIEVEMENT':
case 'GUILD_JOINED_ACHIEVEMENT':
case 'CHALLENGE_JOINED_ACHIEVEMENT':
case 'INVITED_FRIEND_ACHIEVEMENT':

View File

@@ -75,7 +75,7 @@
:class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
) {{ $t('buyNow') }}
div.limitedTime(v-if="item.event")
div.limitedTime(v-if="item.event && item.owned == null")
span.svg-icon.inline.icon-16(v-html="icons.clock")
span.limitedString {{ limitedString }}
@@ -397,6 +397,10 @@
this.$emit('buyPressed', this.item);
this.hideDialog();
if (this.item.key === 'rebirth_orb') {
window.location.reload(true);
}
},
purchaseGems () {
if (this.item.key === 'rebirth_orb') {

View File

@@ -5,7 +5,7 @@ div
slot(name="itemBadge", :item="item", :emptyItem="emptyItem")
span.badge.badge-pill.badge-item.badge-clock(
v-if="item.event && showEventBadge",
v-if="item.event && item.owned == null && showEventBadge",
)
span.svg-icon.inline.clock(v-html="icons.clock")

View File

@@ -1,11 +1,15 @@
<template lang="pug">
.tags-popup
.tags-category.d-flex
.tags-category.d-flex(
v-for="tagsType in tagsByType",
v-if="tagsType.tags.length > 0 || tagsType.key === 'tags'",
:key="tagsType.key"
)
.tags-header
strong(v-once) {{ $t('tags') }}
strong(v-once) {{ $t(tagsType.key) }}
.tags-list.container
.row
.col-4(v-for="tag in tags")
.col-4(v-for="(tag, tagIndex) in tagsType.tags")
.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", :value="tag.id", v-model="selectedTags", :id="`tag-${tag.id}`")
label.custom-control-label(:title="tag.name", :for="`tag-${tag.id}`", v-markdown="tag.name")
@@ -103,6 +107,34 @@ export default {
selectedTags: [],
};
},
computed: {
tagsByType () {
const tagsByType = {
challenges: {
key: 'challenges',
tags: [],
},
groups: {
key: 'groups',
tags: [],
},
user: {
key: 'tags',
tags: [],
},
};
this.$props.tags.forEach(t => {
if (t.group) {
tagsByType.groups.tags.push(t);
} else if (t.challenge) {
tagsByType.challenges.tags.push(t);
} else {
tagsByType.user.tags.push(t);
}
});
return tagsByType;
},
},
watch: {
selectedTags () {
this.$emit('input', this.selectedTags);

View File

@@ -159,6 +159,15 @@
| {{ $t(frequency) }}
.option.group-options(v-if='groupId')
.form-group(v-if="task.type === 'todo'")
label(v-once) {{ $t('sharedCompletion') }}
b-dropdown.inline-dropdown(:text="$t(sharedCompletion)")
b-dropdown-item(
v-for="completionOption in ['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']",
:key="completionOption",
@click="sharedCompletion = completionOption",
:class="{active: sharedCompletion === completionOption}"
) {{ $t(completionOption) }}
.form-group.row
label.col-12(v-once) {{ $t('assignedTo') }}
.col-12.mt-2
@@ -179,23 +188,12 @@
.row
button.btn.btn-primary(@click.stop.prevent="showAssignedSelect = !showAssignedSelect") {{$t('close')}}
.option.group-options(v-if='groupId')
.form-group
label(v-once) {{ $t('approvalRequired') }}
toggle-switch.d-inline-block(
:checked="requiresApproval",
@change="updateRequiresApproval"
)
.form-group(v-if="task.type === 'todo'")
label(v-once) {{ $t('sharedCompletion') }}
b-dropdown.inline-dropdown(:text="$t(sharedCompletion)")
b-dropdown-item(
v-for="completionOption in ['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']",
:key="completionOption",
@click="sharedCompletion = completionOption",
:class="{active: sharedCompletion === completionOption}"
) {{ $t(completionOption) }}
.advanced-settings(v-if="task.type !== 'reward'")
.advanced-settings-toggle.d-flex.justify-content-between.align-items-center(@click = "showAdvancedOptions = !showAdvancedOptions")
@@ -360,8 +358,8 @@
margin-top: 12px;
position: relative;
label {
max-height: 30px;
.custom-control-label p {
word-break: break-word;
}
}
@@ -711,7 +709,7 @@ export default {
calendar: calendarIcon,
}),
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
sharedCompletion: 'recurringCompletion',
sharedCompletion: 'singleCompletion',
members: [],
memberNamesById: {},
assignedMembers: [],
@@ -842,7 +840,7 @@ export default {
});
this.assignedMembers = [];
if (this.task.group && this.task.group.assignedUsers) this.assignedMembers = this.task.group.assignedUsers;
if (this.task.group) this.sharedCompletion = this.task.group.sharedCompletion || 'recurringCompletion';
if (this.task.group) this.sharedCompletion = this.task.group.sharedCompletion || 'singleCompletion';
}
// @TODO: This whole component is mutating a prop and that causes issues. We need to not copy the prop similar to group modals
@@ -926,7 +924,6 @@ export default {
// TODO Fix up permissions on task.group so we don't have to keep doing these hacks
if (this.groupId) {
this.task.group.assignedUsers = this.assignedMembers;
this.task.requiresApproval = this.requiresApproval;
this.task.group.approval.required = this.requiresApproval;
this.task.sharedCompletion = this.sharedCompletion;
@@ -954,6 +951,7 @@ export default {
});
});
Promise.all(promises);
this.task.group.assignedUsers = this.assignedMembers;
this.$emit('taskCreated', this.task);
} else {
this.createTask(this.task);

View File

@@ -201,6 +201,7 @@
.custom-control-label {
margin-left: 10px;
word-break: break-word;
}
.filter-panel-footer {

View File

@@ -51,10 +51,8 @@ export async function set (store, changes) {
}
}
axios.put('/api/v4/user', changes);
// TODO
// .then((res) => console.log('set', res))
// .catch((err) => console.error('set', err));
let response = await axios.put('/api/v4/user', changes);
return response.data.data;
}
export async function sleep (store) {

View File

@@ -285,7 +285,8 @@
"claim": "Claim",
"removeClaim": "Remove Claim",
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
"yourTaskHasBeenApproved": "Your task <span class=\"notification-green\"><%= taskText %></span> has been approved.",
"youHaveBeenAssignedTask": "<%= managerName %> has assigned you the task <span class=\"notification-bold\"><%= taskText %></span>.",
"yourTaskHasBeenApproved": "Your task <span class=\"notification-green notification-bold\"><%= taskText %></span> has been approved.",
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> marked <span class=\"notification-bold\"><%= taskText %></span> as needing additional work.",
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> requests approval for <span class=\"notification-bold\"><%= taskName %></span>",
"approve": "Approve",

View File

@@ -173,7 +173,7 @@
"habitCounterDown": "Negative Counter (Resets <%= frequency %>)",
"taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested",
"taskApprovalHasBeenRequested": "Approval has been requested",
"taskApprovalWasNotRequested": "Only a task waiting for approval can be marked as needing more work",
"taskApprovalWasNotRequested": "Approval has not been requested for this task.",
"approvals": "Approvals",
"approvalRequired": "Needs Approval",
"repeatZero": "Daily is never due",

View File

@@ -8,7 +8,7 @@ import takeThisGear from './special-takeThis';
import wonderconGear from './special-wondercon';
import t from '../../../translation';
const CURRENT_SEASON = 'spring';
const CURRENT_SEASON = '_NONE_';
let armor = {
0: backerGear.armorSpecial0,

View File

@@ -3,7 +3,7 @@ import defaults from 'lodash/defaults';
import each from 'lodash/each';
import t from './translation';
const CURRENT_SEASON = 'March';
const CURRENT_SEASON = '_NONE_';
let drops = {
Base: {

View File

@@ -511,7 +511,7 @@ let quests = {
value: 1,
category: 'pet',
canBuy () {
return true;
return false;
},
collect: {
plainEgg: {

View File

@@ -8,16 +8,16 @@ const featuredItems = {
path: 'armoire',
},
{
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.Celestial',
type: 'eggs',
path: 'eggs.Dragon',
},
{
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.Rainbow',
type: 'hatchingPotions',
path: 'hatchingPotions.Red',
},
{
type: 'card',
path: 'cardTypes.goodluck',
path: 'cardTypes.congrats',
},
],
quests: [

View File

@@ -1,30 +1,23 @@
import { SEASONAL_SETS } from '../content/constants';
// import { SEASONAL_SETS } from '../content/constants';
module.exports = {
opened: true,
opened: false,
currentSeason: 'Spring',
currentSeason: 'Closed',
dateRange: { start: '2019-03-19', end: '2019-04-30' },
dateRange: { start: '2018-09-20', end: '2018-10-31' },
availableSets: [
...SEASONAL_SETS.spring,
],
pinnedSets: {
wizard: 'spring2019AmberMageSet',
warrior: 'spring2019OrchidWarriorSet',
rogue: 'spring2019CloudRogueSet',
healer: 'spring2019RobinHealerSet',
},
availableSpells: [
'shinySeed',
],
availableQuests: [
'egg',
],
featuredSet: 'spring2018DucklingRogueSet',
featuredSet: 'mummyMedicSet',
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -7,7 +7,10 @@ import {
import _ from 'lodash';
import apiError from '../../libs/apiError';
import validator from 'validator';
import { validateItemPath } from '../../libs/items/utils';
import {
validateItemPath,
castItemVal,
} from '../../libs/items/utils';
let api = {};
@@ -271,7 +274,7 @@ api.updateHero = {
hero.markModified('items.pets');
}
if (updateData.itemPath && updateData.itemVal && validateItemPath(updateData.itemPath)) {
_.set(hero, updateData.itemPath, updateData.itemVal); // Sanitization at 5c30944 (deemed unnecessary)
_.set(hero, updateData.itemPath, castItemVal(updateData.itemPath, updateData.itemVal)); // Sanitization at 5c30944 (deemed unnecessary)
}
if (updateData.auth && updateData.auth.blocked === true) {

View File

@@ -12,6 +12,7 @@ let api = {};
* @apiDescription Get inbox messages for a user
*
* @apiParam (Query) {Number} page Load the messages of the selected Page - 10 Messages per Page
* @apiParam (Query) {GUID} conversation Loads only the messages of a conversation
*
* @apiSuccess {Array} data An array of inbox messages
*/
@@ -22,9 +23,10 @@ api.getInboxMessages = {
async handler (req, res) {
const user = res.locals.user;
const page = req.query.page;
const conversation = req.query.conversation;
const userInbox = await inboxLib.getUserInbox(user, {
page,
page, conversation,
});
res.respond(200, userInbox);

View File

@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
let api = {};
// @TODO export this const, cannot export it from here because only routes are exported from controllers
const LAST_ANNOUNCEMENT_TITLE = 'APRIL SUBSCRIBER ITEMS!';
const LAST_ANNOUNCEMENT_TITLE = 'MAY 2019 RESOLUTION SUCCESS CHALLENGE AND NEW TAKE THIS CHALLENGE!';
const worldDmg = { // @TODO
bailey: false,
};
@@ -30,13 +30,18 @@ api.getNews = {
<div class="mr-3 ${baileyClass}"></div>
<div class="media-body">
<h1 class="align-self-center">${res.t('newStuff')}</h1>
<h2>4/25/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
<h2>4/30/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
</div>
</div>
<hr/>
<p>The April Subscriber Item Set has been revealed: <a href='/user/settings/subscription'>the Opulent Opal Item Set</a>! You only have until April 30 to receive the item set when you subscribe. If you're already an active subscriber, reload the site and then head to Inventory > Items to claim your gear!</p>
<p>Subscribers also receive the ability to buy Gems for Gold -- the longer you subscribe, the more gems you can buy per month! There are other perks as well, such as longer access to uncompressed data and a cute Jackalope pet. Best of all, subscriptions let us keep Habitica running. Thank you very much for your support -- it means a lot to us.</p>
<div class="small mb-3">by Beffymaroo</div>
<div class="scene_spells center-block"></div>
<p>The Habitica team has launched a special official Challenge series hosted in the <a href='/groups/guild/6e6a8bd3-9f5f-4351-9188-9f11fcd80a99' target='_blank'>Official New Year's Resolution Guild</a>. These Challenges are designed to help you build and maintain goals that are destined for success and then stick with them as the year progresses. For this month's Challenge, (Review Your Combat Tactics) [https://habitica.com/challenges/f64a1afa-ae00-4855-91af-b52e9bd6803f], we're focusing on refining your strategy to help you stay motivated and keep moving forward as we're almost halfway through the year!! It has a 15 Gem prize, which will be awarded to five lucky winners on June 3.</p>
<p>Congratulations to the winners of April's Challenge: punkshep, Syntrillium, BardoVelho, Betsy, and Baileythebookworm!</p>
<p>The next Take This Challenge has also launched, "<a href='/challenges/fd1beea9-d92f-4f8f-b58c-8f5319991ad1'>Organize Your Inventory!</a>", with a focus on decluttering your living space. Be sure to check it out to earn additional pieces of the Take This armor set!</p>
<p><a href='http://www.takethis.org/' target='_blank'>Take This</a> is a nonprofit that seeks to inform the gamer community about mental health issues, to provide education about mental disorders and mental illness prevention, and to reduce the stigma of mental illness.</p>
<p>Congratulations to the winners of the last Take This Challenge, "Harder, Faster, Stronger!": grand prize winner Evan Cowan, and runners-up ResearcherLilly, corinnetags, Lucy, mrdarq, and Snarky. Plus, all participants in that Challenge have received a piece of the <a href='http://habitica.wikia.com/wiki/Event_Item_Sequences#Take_This_Armor_Set' target='_blank'>Take This item set</a> if they hadn't completed it already. It is located in your Rewards column. Enjoy!</p>
<div class="small mb-3">by Doctor B, the Take This team, Lemoness, Beffymaroo, and SabreCat</div>
<div class="promo_take_this center-block"></div>
</div>
`,
});

View File

@@ -206,6 +206,14 @@ api.assignTask = {
let message = res.t('userIsClamingTask', {username: user.profile.name, task: task.text});
const newMessage = group.sendChat(message);
promises.push(newMessage.save());
} else {
const taskText = task.text;
const managerName = user.profile.name;
assignedUser.addNotification('GROUP_TASK_ASSIGNED', {
message: res.t('youHaveBeenAssignedTask', {managerName, taskText}),
taskId: task._id,
});
}
promises.push(group.syncTask(task, assignedUser));
@@ -261,6 +269,15 @@ api.unassignTask = {
await group.unlinkTask(task, assignedUser);
let notificationIndex = assignedUser.notifications.findIndex(function findNotification (notification) {
return notification && notification.data && notification.type === 'GROUP_TASK_ASSIGNED' && notification.data.taskId === task._id;
});
if (notificationIndex !== -1) {
assignedUser.notifications.splice(notificationIndex, 1);
await assignedUser.save();
}
res.respond(200, task);
},
};
@@ -308,6 +325,9 @@ api.approveTask = {
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (task.group.approval.approved === true) throw new NotAuthorized(res.t('canOnlyApproveTaskOnce'));
if (!task.group.approval.requested) {
throw new NotAuthorized(res.t('taskApprovalWasNotRequested'));
}
task.group.approval.dateApproved = new Date();
task.group.approval.approvingUser = user._id;

View File

@@ -72,4 +72,25 @@ api.clearMessages = {
},
};
/**
* @api {get} /inbox/conversations Get the conversations for a user
* @apiName conversations
* @apiGroup Inbox
* @apiDescription Get the conversations for a user
*
* @apiSuccess {Array} data An array of inbox conversations
*/
api.conversations = {
method: 'GET',
middlewares: [authWithHeaders()],
url: '/inbox/conversations',
async handler (req, res) {
const user = res.locals.user;
const result = await inboxLib.listConversations(user);
res.respond(200, result);
},
};
module.exports = api;

View File

@@ -1,14 +1,25 @@
import { inboxModel as Inbox } from '../../models/message';
import {inboxModel as Inbox} from '../../models/message';
import {
model as User,
} from '../../models/user';
import orderBy from 'lodash/orderBy';
import keyBy from 'lodash/keyBy';
const PM_PER_PAGE = 10;
export async function getUserInbox (user, options = {asArray: true, page: 0}) {
export async function getUserInbox (user, options = {asArray: true, page: 0, conversation: null}) {
if (typeof options.asArray === 'undefined') {
options.asArray = true;
}
const findObj = {ownerId: user._id};
if (options.conversation) {
findObj.uuid = options.conversation;
}
let query = Inbox
.find({ownerId: user._id})
.find(findObj)
.sort({timestamp: -1});
if (typeof options.page !== 'undefined') {
@@ -29,12 +40,45 @@ export async function getUserInbox (user, options = {asArray: true, page: 0}) {
}
}
export async function listConversations (user) {
let query = Inbox
.aggregate([
{
$match: {
ownerId: user._id,
},
},
{
$group: {
_id: '$uuid',
timestamp: {$max: '$timestamp'}, // sort before group doesn't work - use the max value to sort it again after
},
},
]);
const conversationsList = orderBy(await query.exec(), ['timestamp'], ['desc']).map(c => c._id);
const users = await User.find({_id: {$in: conversationsList}})
.select('_id profile.name auth.local.username')
.lean()
.exec();
const usersMap = keyBy(users, '_id');
const conversations = conversationsList.map(userId => ({
uuid: usersMap[userId]._id,
user: usersMap[userId].profile.name,
username: usersMap[userId].auth.local.username,
}));
return conversations;
}
export async function getUserInboxMessage (user, messageId) {
return Inbox.findOne({ownerId: user._id, _id: messageId}).exec();
}
export async function deleteMessage (user, messageId) {
const message = await Inbox.findOne({_id: messageId, ownerId: user._id }).exec();
const message = await Inbox.findOne({_id: messageId, ownerId: user._id}).exec();
if (!message) return false;
await Inbox.remove({_id: message._id, ownerId: user._id}).exec();

View File

@@ -54,4 +54,24 @@ export function validateItemPath (itemPath) {
if (itemPath.indexOf('items.quests') === 0) {
return Boolean(shared.content.quests[key]);
}
}
// When passed a value of an item in the user object it'll convert the
// value to the correct format.
// Example a numeric string like "5" applied to a food item (expecting an interger)
// will be converted to the number 5
// TODO cast the correct value for `items.gear.owned`
export function castItemVal (itemPath, itemVal) {
if (
itemPath.indexOf('items.pets') === 0 ||
itemPath.indexOf('items.eggs') === 0 ||
itemPath.indexOf('items.hatchingPotions') === 0 ||
itemPath.indexOf('items.food') === 0 ||
itemPath.indexOf('items.mounts') === 0 ||
itemPath.indexOf('items.quests') === 0
) {
return Number(itemVal);
}
return itemVal;
}

View File

@@ -62,7 +62,7 @@ schema.pre('init', function ensureSummaryIsFetched (chal) {
});
// A list of additional fields that cannot be updated (but can be set on creation)
let noUpdate = ['group', 'official', 'shortName', 'prize'];
let noUpdate = ['group', 'leader', 'official', 'shortName', 'prize'];
schema.statics.sanitizeUpdate = function sanitizeUpdate (updateObj) {
return this.sanitize(updateObj, noUpdate);
};

View File

@@ -1341,7 +1341,7 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
matchingTask.group.id = taskToSync.group.id;
matchingTask.userId = user._id;
matchingTask.group.taskId = taskToSync._id;
user.tasksOrder[`${taskToSync.type}s`].push(matchingTask._id);
user.tasksOrder[`${taskToSync.type}s`].unshift(matchingTask._id);
} else {
_.merge(matchingTask, syncableAttrs(taskToSync));
// Make sure the task is in user.tasksOrder
@@ -1419,9 +1419,29 @@ schema.methods.removeTask = async function groupRemoveTask (task) {
$set: {'group.broken': 'TASK_DELETED'},
}, {multi: true}).exec();
// Get Managers
const managerIds = Object.keys(group.managers);
managerIds.push(group.leader);
const managers = await User.find({_id: managerIds}, 'notifications').exec(); // Use this method so we can get access to notifications
// Remove old notifications
let removalPromises = [];
managers.forEach((manager) => {
let notificationIndex = manager.notifications.findIndex(function findNotification (notification) {
return notification && notification.data && notification.data.groupTaskId === task._id && notification.type === 'GROUP_TASK_APPROVAL';
});
if (notificationIndex !== -1) {
manager.notifications.splice(notificationIndex, 1);
removalPromises.push(manager.save());
}
});
removeFromArray(group.tasksOrder[`${task.type}s`], task._id);
group.markModified('tasksOrder');
return await group.save();
removalPromises.push(group.save());
return await Promise.all(removalPromises);
};
// Returns true if the user has reached the spam message limit

View File

@@ -113,7 +113,7 @@ export let TaskSchema = new Schema({
requested: {$type: Boolean, default: false},
requestedDate: {$type: Date},
},
sharedCompletion: {$type: String, enum: _.values(SHARED_COMPLETION), default: SHARED_COMPLETION.default},
sharedCompletion: {$type: String, enum: _.values(SHARED_COMPLETION), default: SHARED_COMPLETION.single},
},
reminders: [{

View File

@@ -15,6 +15,7 @@ const NOTIFICATION_TYPES = [
'CRON',
'GROUP_TASK_APPROVAL',
'GROUP_TASK_APPROVED',
'GROUP_TASK_ASSIGNED',
'GROUP_TASK_NEEDS_WORK',
'LOGIN_INCENTIVE',
'GROUP_INVITE_ACCEPTED',