mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-04-22 19:39:25 -05:00
Merge branch 'develop' into release
This commit is contained in:
+10
-3
@@ -1,3 +1,10 @@
|
||||
web:
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
client:
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
|
||||
server:
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
|
||||
+35
-12
@@ -1,13 +1,36 @@
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
links:
|
||||
- mongo
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
client:
|
||||
build: .
|
||||
networks:
|
||||
- habitica
|
||||
environment:
|
||||
- BASE_URL=http://server:3000
|
||||
ports:
|
||||
- "8080:8080"
|
||||
command: ["npm", "run", "client:dev"]
|
||||
depends_on:
|
||||
- server
|
||||
|
||||
server:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
- habitica
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
depends_on:
|
||||
- mongo
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
networks:
|
||||
- habitica
|
||||
|
||||
networks:
|
||||
habitica:
|
||||
driver: bridge
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
const UserNotification = require('../website/server/models/userNotification').model;
|
||||
const content = require('../website/common/script/content/index');
|
||||
|
||||
const migrationName = '20180125_clean_new_migrations';
|
||||
const authorName = 'paglias'; // in case script author needs to know when their ...
|
||||
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
|
||||
|
||||
/*
|
||||
* Clean new migration types for processed users
|
||||
*/
|
||||
|
||||
const monk = require('monk');
|
||||
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
const dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const types = ['NEW_MYSTERY_ITEMS', 'CARD_RECEIVED', 'NEW_CHAT_MESSAGE'];
|
||||
|
||||
dbUsers.update({_id: user._id}, {
|
||||
$pull: {notifications: { type: {$in: types } } },
|
||||
$set: {migration: migrationName},
|
||||
});
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
|
||||
if (user._id === authorUuid) console.warn(`${authorName } processed`);
|
||||
}
|
||||
|
||||
function exiting (code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) {
|
||||
msg = 'ERROR!';
|
||||
}
|
||||
if (msg) {
|
||||
if (code) {
|
||||
console.error(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function displayData () {
|
||||
console.warn(`\n${ count } users processed\n`);
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
const userPromises = users.map(updateUser);
|
||||
const lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(() => {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
const query = {
|
||||
migration: {$ne: migrationName},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2010-01-24')},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId,
|
||||
};
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return exiting(1, `ERROR! ${ err}`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -0,0 +1,149 @@
|
||||
const UserNotification = require('../website/server/models/userNotification').model;
|
||||
const content = require('../website/common/script/content/index');
|
||||
|
||||
const migrationName = '20180125_migrations-v2';
|
||||
const authorName = 'paglias'; // in case script author needs to know when their ...
|
||||
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
|
||||
|
||||
/*
|
||||
* Migrate to new notifications system
|
||||
*/
|
||||
|
||||
const monk = require('monk');
|
||||
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
const dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const notifications = [];
|
||||
|
||||
// UNALLOCATED_STATS_POINTS skipped because added on each save
|
||||
// NEW_STUFF skipped because it's a new type
|
||||
// GROUP_TASK_NEEDS_WORK because it's a new type
|
||||
// NEW_INBOX_MESSAGE not implemented yet
|
||||
|
||||
|
||||
// NEW_MYSTERY_ITEMS
|
||||
const mysteryItems = user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems;
|
||||
if (Array.isArray(mysteryItems) && mysteryItems.length > 0) {
|
||||
const newMysteryNotif = new UserNotification({
|
||||
type: 'NEW_MYSTERY_ITEMS',
|
||||
data: {
|
||||
items: mysteryItems,
|
||||
},
|
||||
}).toJSON();
|
||||
notifications.push(newMysteryNotif);
|
||||
}
|
||||
|
||||
// CARD_RECEIVED
|
||||
Object.keys(content.cardTypes).forEach(cardType => {
|
||||
const existingCards = user.items.special[`${cardType}Received`] || [];
|
||||
existingCards.forEach(sender => {
|
||||
const newNotif = new UserNotification({
|
||||
type: 'CARD_RECEIVED',
|
||||
data: {
|
||||
card: cardType,
|
||||
from: {
|
||||
// id is missing in old notifications
|
||||
name: sender,
|
||||
},
|
||||
},
|
||||
}).toJSON();
|
||||
|
||||
notifications.push(newNotif);
|
||||
});
|
||||
});
|
||||
|
||||
// NEW_CHAT_MESSAGE
|
||||
Object.keys(user.newMessages).forEach(groupId => {
|
||||
const existingNotif = user.newMessages[groupId];
|
||||
|
||||
if (existingNotif) {
|
||||
const newNotif = new UserNotification({
|
||||
type: 'NEW_CHAT_MESSAGE',
|
||||
data: {
|
||||
group: {
|
||||
id: groupId,
|
||||
name: existingNotif.name,
|
||||
},
|
||||
},
|
||||
}).toJSON();
|
||||
|
||||
notifications.push(newNotif);
|
||||
}
|
||||
});
|
||||
|
||||
dbUsers.update({_id: user._id}, {
|
||||
$push: {notifications: { $each: notifications } },
|
||||
$set: {migration: migrationName},
|
||||
});
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
|
||||
if (user._id === authorUuid) console.warn(`${authorName } processed`);
|
||||
}
|
||||
|
||||
function exiting (code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) {
|
||||
msg = 'ERROR!';
|
||||
}
|
||||
if (msg) {
|
||||
if (code) {
|
||||
console.error(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function displayData () {
|
||||
console.warn(`\n${ count } users processed\n`);
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
const userPromises = users.map(updateUser);
|
||||
const lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(() => {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
const query = {
|
||||
migration: {$ne: migrationName},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2010-01-24')},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId,
|
||||
};
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return exiting(1, `ERROR! ${ err}`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -17,5 +17,5 @@ function setUpServer () {
|
||||
setUpServer();
|
||||
|
||||
// Replace this with your migration
|
||||
const processUsers = require('./tasks/tasks-set-everyX');
|
||||
const processUsers = require('./20180125_clean_new_notifications.js');
|
||||
processUsers();
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
var UserNotification = require('../website/server/models/userNotification').model
|
||||
|
||||
var _id = '';
|
||||
|
||||
var items = ['back_mystery_201801','headAccessory_mystery_201801']
|
||||
|
||||
var update = {
|
||||
$addToSet: {
|
||||
'purchased.plan.mysteryItems':{
|
||||
$each:['back_mystery_201801','headAccessory_mystery_201801']
|
||||
$each: items,
|
||||
}
|
||||
}
|
||||
},
|
||||
$push: {
|
||||
notifications: (new UserNotification({
|
||||
type: 'NEW_MYSTERY_ITEMS',
|
||||
data: {
|
||||
items: items,
|
||||
},
|
||||
})).toJSON(),
|
||||
},
|
||||
};
|
||||
|
||||
/*var update = {
|
||||
|
||||
Generated
+382
-362
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -67,7 +67,7 @@
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.13.0",
|
||||
"moment-recur": "git://github.com/habitrpg/moment-recur.git#f147ef27bbc26ca67638385f3db4a44084c76626",
|
||||
"mongoose": "~4.8.6",
|
||||
"mongoose": "^4.8.6",
|
||||
"mongoose-id-autoinc": "~2013.7.14-4",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "~0.8.2",
|
||||
@@ -186,7 +186,7 @@
|
||||
"phantomjs-prebuilt": "^2.1.12",
|
||||
"require-again": "^2.0.0",
|
||||
"selenium-server": "^3.0.1",
|
||||
"sinon": "^1.17.2",
|
||||
"sinon": "^4.2.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"sinon-stub-promise": "^4.0.0",
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /challenges/:challengeId/clone', () => {
|
||||
it('clones a challenge', async () => {
|
||||
const user = await generateUser({balance: 10});
|
||||
const group = await generateGroup(user);
|
||||
|
||||
const name = 'Test Challenge';
|
||||
const shortName = 'TC Label';
|
||||
const description = 'Test Description';
|
||||
const prize = 1;
|
||||
|
||||
const challenge = await user.post('/challenges', {
|
||||
group: group._id,
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
prize,
|
||||
});
|
||||
const challengeTask = await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
const cloneChallengeResponse = await user.post(`/challenges/${challenge._id}/clone`, {
|
||||
group: group._id,
|
||||
name: `${name} cloned`,
|
||||
shortName,
|
||||
description,
|
||||
prize,
|
||||
});
|
||||
|
||||
expect(cloneChallengeResponse.clonedTasks[0].text).to.eql(challengeTask.text);
|
||||
expect(cloneChallengeResponse.clonedTasks[0]._id).to.not.eql(challengeTask._id);
|
||||
expect(cloneChallengeResponse.clonedTasks[0].challenge.id).to.eql(cloneChallengeResponse.clonedChallenge._id);
|
||||
});
|
||||
});
|
||||
@@ -426,6 +426,9 @@ describe('POST /chat', () => {
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.exist;
|
||||
expect(memberWithNotification.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id;
|
||||
})).to.exist;
|
||||
});
|
||||
|
||||
it('notifies other users of new messages for a party', async () => {
|
||||
@@ -443,6 +446,9 @@ describe('POST /chat', () => {
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
|
||||
expect(memberWithNotification.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === group._id;
|
||||
})).to.exist;
|
||||
});
|
||||
|
||||
context('Spam prevention', () => {
|
||||
|
||||
@@ -24,10 +24,13 @@ describe('POST /groups/:id/chat/seen', () => {
|
||||
});
|
||||
|
||||
it('clears new messages for a guild', async () => {
|
||||
await guildMember.sync();
|
||||
const initialNotifications = guildMember.notifications.length;
|
||||
await guildMember.post(`/groups/${guild._id}/chat/seen`);
|
||||
|
||||
let guildThatHasSeenChat = await guildMember.get('/user');
|
||||
|
||||
expect(guildThatHasSeenChat.notifications.length).to.equal(initialNotifications - 1);
|
||||
expect(guildThatHasSeenChat.newMessages).to.be.empty;
|
||||
});
|
||||
});
|
||||
@@ -53,10 +56,13 @@ describe('POST /groups/:id/chat/seen', () => {
|
||||
});
|
||||
|
||||
it('clears new messages for a party', async () => {
|
||||
await partyMember.sync();
|
||||
const initialNotifications = partyMember.notifications.length;
|
||||
await partyMember.post(`/groups/${party._id}/chat/seen`);
|
||||
|
||||
let partyMemberThatHasSeenChat = await partyMember.get('/user');
|
||||
|
||||
expect(partyMemberThatHasSeenChat.notifications.length).to.equal(initialNotifications - 1);
|
||||
expect(partyMemberThatHasSeenChat.newMessages).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,13 +70,21 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
it('removes new messages for that group from user', async () => {
|
||||
await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
await leader.sync();
|
||||
|
||||
expect(leader.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id;
|
||||
})).to.exist;
|
||||
expect(leader.newMessages[groupToLeave._id]).to.not.be.empty;
|
||||
|
||||
await leader.post(`/groups/${groupToLeave._id}/leave`);
|
||||
await leader.sync();
|
||||
|
||||
expect(leader.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id;
|
||||
})).to.not.exist;
|
||||
expect(leader.newMessages[groupToLeave._id]).to.be.empty;
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
sleep,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
@@ -188,13 +189,20 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
|
||||
it('removes new messages from a member who is removed', async () => {
|
||||
await partyLeader.post(`/groups/${party._id}/chat`, { message: 'Some message' });
|
||||
await sleep(0.5);
|
||||
await removedMember.sync();
|
||||
|
||||
expect(removedMember.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === party._id;
|
||||
})).to.exist;
|
||||
expect(removedMember.newMessages[party._id]).to.not.be.empty;
|
||||
|
||||
await partyLeader.post(`/groups/${party._id}/removeMember/${removedMember._id}`);
|
||||
await removedMember.sync();
|
||||
|
||||
expect(removedMember.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === party._id;
|
||||
})).to.not.exist;
|
||||
expect(removedMember.newMessages[party._id]).to.be.empty;
|
||||
});
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
id: group._id,
|
||||
name: groupName,
|
||||
inviter: inviter._id,
|
||||
publicGuild: false,
|
||||
}]);
|
||||
|
||||
await expect(userToInvite.get('/user'))
|
||||
@@ -127,11 +128,13 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
id: group._id,
|
||||
name: groupName,
|
||||
inviter: inviter._id,
|
||||
publicGuild: false,
|
||||
},
|
||||
{
|
||||
id: group._id,
|
||||
name: groupName,
|
||||
inviter: inviter._id,
|
||||
publicGuild: false,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ describe('POST /members/send-private-message', () => {
|
||||
|
||||
it('sends a private message to a user', async () => {
|
||||
let receiver = await generateUser();
|
||||
// const initialNotifications = receiver.notifications.length;
|
||||
|
||||
await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
@@ -115,10 +116,44 @@ describe('POST /members/send-private-message', () => {
|
||||
return message.uuid === receiver._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
// @TODO waiting for mobile support
|
||||
// expect(updatedReceiver.notifications.length).to.equal(initialNotifications + 1);
|
||||
// const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];
|
||||
|
||||
// expect(notification.type).to.equal('NEW_INBOX_MESSAGE');
|
||||
// expect(notification.data.messageId).to.equal(sendersMessageInReceiversInbox.id);
|
||||
// expect(notification.data.excerpt).to.equal(messageToSend);
|
||||
// expect(notification.data.sender.id).to.equal(updatedSender._id);
|
||||
// expect(notification.data.sender.name).to.equal(updatedSender.profile.name);
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
// @TODO waiting for mobile support
|
||||
xit('creates a notification with an excerpt if the message is too long', async () => {
|
||||
let receiver = await generateUser();
|
||||
let longerMessageToSend = 'A very long message, that for sure exceeds the limit of 100 chars for the excerpt that we set to 100 chars';
|
||||
let messageExcerpt = `${longerMessageToSend.substring(0, 100)}...`;
|
||||
|
||||
await userToSendMessage.post('/members/send-private-message', {
|
||||
message: longerMessageToSend,
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
let updatedReceiver = await receiver.get('/user');
|
||||
|
||||
let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => {
|
||||
return message.uuid === userToSendMessage._id && message.text === longerMessageToSend;
|
||||
});
|
||||
|
||||
const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];
|
||||
|
||||
expect(notification.type).to.equal('NEW_INBOX_MESSAGE');
|
||||
expect(notification.data.messageId).to.equal(sendersMessageInReceiversInbox.id);
|
||||
expect(notification.data.excerpt).to.equal(messageExcerpt);
|
||||
});
|
||||
|
||||
it('allows admin to send when sender has blocked the admin', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'contributor.admin': 1,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
requester,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('GET /news', () => {
|
||||
let api;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
});
|
||||
|
||||
it('returns the latest news in html format, does not require authentication', async () => {
|
||||
const res = await api.get('/news');
|
||||
expect(res).to.be.a.string;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /news/tell-me-later', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'flags.newStuff': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('marks new stuff as read and adds notification', async () => {
|
||||
expect(user.flags.newStuff).to.equal(true);
|
||||
const initialNotifications = user.notifications.length;
|
||||
|
||||
await user.post('/news/tell-me-later');
|
||||
await user.sync();
|
||||
|
||||
expect(user.flags.newStuff).to.equal(false);
|
||||
expect(user.notifications.length).to.equal(initialNotifications + 1);
|
||||
|
||||
const notification = user.notifications[user.notifications.length - 1];
|
||||
|
||||
expect(notification.type).to.equal('NEW_STUFF');
|
||||
// should be marked as seen by default so it's not counted in the number of notifications
|
||||
expect(notification.seen).to.equal(true);
|
||||
expect(notification.data.title).to.be.a.string;
|
||||
});
|
||||
|
||||
it('never adds two notifications', async () => {
|
||||
const initialNotifications = user.notifications.length;
|
||||
|
||||
await user.post('/news/tell-me-later');
|
||||
await user.post('/news/tell-me-later');
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(initialNotifications + 1);
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,7 @@ describe('POST /notifications/:notificationId/read', () => {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
seen: false,
|
||||
}]);
|
||||
|
||||
await user.sync();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /notifications/:notificationId/see', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('errors when notification is not found', async () => {
|
||||
let dummyId = generateUUID();
|
||||
|
||||
await expect(user.post(`/notifications/${dummyId}/see`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageNotificationNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('mark a notification as seen', async () => {
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
|
||||
const id = generateUUID();
|
||||
const id2 = generateUUID();
|
||||
|
||||
await user.update({
|
||||
notifications: [{
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
}, {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
}],
|
||||
});
|
||||
|
||||
const userObj = await user.get('/user'); // so we can check that defaults have been applied
|
||||
expect(userObj.notifications.length).to.equal(2);
|
||||
expect(userObj.notifications[0].seen).to.equal(false);
|
||||
|
||||
const res = await user.post(`/notifications/${id}/see`);
|
||||
expect(res).to.deep.equal({
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
seen: true,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[0].id).to.equal(id);
|
||||
expect(user.notifications[0].seen).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /notifications/:notificationId/read', () => {
|
||||
describe('POST /notifications/read', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
@@ -57,6 +57,7 @@ describe('POST /notifications/:notificationId/read', () => {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
seen: false,
|
||||
}]);
|
||||
|
||||
await user.sync();
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /notifications/see', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('errors when notification is not found', async () => {
|
||||
let dummyId = generateUUID();
|
||||
|
||||
await expect(user.post('/notifications/see', {
|
||||
notificationIds: [dummyId],
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageNotificationNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('mark multiple notifications as seen', async () => {
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
|
||||
const id = generateUUID();
|
||||
const id2 = generateUUID();
|
||||
const id3 = generateUUID();
|
||||
|
||||
await user.update({
|
||||
notifications: [{
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
seen: false,
|
||||
}, {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
seen: false,
|
||||
}, {
|
||||
id: id3,
|
||||
type: 'CRON',
|
||||
data: {},
|
||||
seen: false,
|
||||
}],
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
|
||||
const res = await user.post('/notifications/see', {
|
||||
notificationIds: [id, id3],
|
||||
});
|
||||
|
||||
expect(res).to.deep.equal([
|
||||
{
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
seen: true,
|
||||
}, {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
seen: false,
|
||||
}, {
|
||||
id: id3,
|
||||
type: 'CRON',
|
||||
data: {},
|
||||
seen: true,
|
||||
}]);
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
expect(user.notifications[0].id).to.equal(id);
|
||||
expect(user.notifications[0].seen).to.equal(true);
|
||||
|
||||
expect(user.notifications[1].id).to.equal(id2);
|
||||
expect(user.notifications[1].seen).to.equal(false);
|
||||
|
||||
expect(user.notifications[2].id).to.equal(id3);
|
||||
expect(user.notifications[2].seen).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let {group, members, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when user is not assigned', async () => {
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when user is not the group leader', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await expect(member.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('marks as task as needing more work', async () => {
|
||||
const initialNotifications = member.notifications.length;
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// score task to require approval
|
||||
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}/needs-work/${member._id}`);
|
||||
|
||||
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// Check that the notification approval request has been removed
|
||||
expect(syncedTask.group.approval.requested).to.equal(false);
|
||||
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
|
||||
|
||||
// Check that the notification is correct
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 1);
|
||||
const notification = member.notifications[member.notifications.length - 1];
|
||||
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
|
||||
|
||||
const taskText = syncedTask.text;
|
||||
const managerName = user.profile.name;
|
||||
|
||||
expect(notification.data.message).to.equal(t('taskNeedsWork', {taskText, managerName}));
|
||||
|
||||
expect(notification.data.task.id).to.equal(syncedTask._id);
|
||||
expect(notification.data.task.text).to.equal(taskText);
|
||||
|
||||
expect(notification.data.group.id).to.equal(syncedTask.group.id);
|
||||
expect(notification.data.group.name).to.equal(guild.name);
|
||||
|
||||
expect(notification.data.manager.id).to.equal(user._id);
|
||||
expect(notification.data.manager.name).to.equal(managerName);
|
||||
|
||||
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.find(n => {
|
||||
n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
|
||||
})).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('allows a manager to mark a task as needing work', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// score task to require approval
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
const initialNotifications = member.notifications.length;
|
||||
|
||||
await member2.post(`/tasks/${task._id}/needs-work/${member._id}`);
|
||||
|
||||
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// Check that the notification approval request has been removed
|
||||
expect(syncedTask.group.approval.requested).to.equal(false);
|
||||
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
|
||||
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 1);
|
||||
const notification = member.notifications[member.notifications.length - 1];
|
||||
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
|
||||
|
||||
const taskText = syncedTask.text;
|
||||
const managerName = member2.profile.name;
|
||||
|
||||
expect(notification.data.message).to.equal(t('taskNeedsWork', {taskText, managerName}));
|
||||
|
||||
expect(notification.data.task.id).to.equal(syncedTask._id);
|
||||
expect(notification.data.task.text).to.equal(taskText);
|
||||
|
||||
expect(notification.data.group.id).to.equal(syncedTask.group.id);
|
||||
expect(notification.data.group.name).to.equal(guild.name);
|
||||
|
||||
expect(notification.data.manager.id).to.equal(member2._id);
|
||||
expect(notification.data.manager.name).to.equal(managerName);
|
||||
|
||||
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
|
||||
await Promise.all([user.sync(), member2.sync()]);
|
||||
|
||||
expect(user.notifications.find(n => {
|
||||
n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
|
||||
})).to.equal(undefined);
|
||||
|
||||
expect(member2.notifications.find(n => {
|
||||
n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
|
||||
})).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('prevents marking a task as needing work if it was already approved', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
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({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('canOnlyApproveTaskOnce'),
|
||||
});
|
||||
});
|
||||
|
||||
it('prevents marking a task as needing work if it is not waiting for approval', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalWasNotRequested'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ describe('GET /user/anonymized', () => {
|
||||
'achievements.challenges': 'some',
|
||||
'inbox.messages': [{ text: 'some text' }],
|
||||
tags: [{ name: 'some name', challenge: 'some challenge' }],
|
||||
notifications: [],
|
||||
});
|
||||
|
||||
await generateHabit({ userId: user._id });
|
||||
@@ -65,6 +66,7 @@ describe('GET /user/anonymized', () => {
|
||||
expect(returnedUser.stats.toNextLevel).to.eql(common.tnl(user.stats.lvl));
|
||||
expect(returnedUser.stats.maxMP).to.eql(30); // TODO why 30?
|
||||
expect(returnedUser.newMessages).to.not.exist;
|
||||
expect(returnedUser.notifications).to.not.exist;
|
||||
expect(returnedUser.profile).to.not.exist;
|
||||
expect(returnedUser.purchased.plan).to.not.exist;
|
||||
expect(returnedUser.contributor).to.not.exist;
|
||||
|
||||
@@ -13,15 +13,20 @@ describe('POST /user/open-mystery-item', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'purchased.plan.mysteryItems': [mysteryItemKey],
|
||||
notifications: [
|
||||
{type: 'NEW_MYSTERY_ITEMS', data: { items: [mysteryItemKey] }},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// More tests in common code unit tests
|
||||
|
||||
it('opens a mystery item', async () => {
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
let response = await user.post('/user/open-mystery-item');
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
expect(user.items.gear.owned[mysteryItemKey]).to.be.true;
|
||||
expect(response.message).to.equal(t('mysteryItemOpened'));
|
||||
expect(response.data.key).to.eql(mysteryItemKey);
|
||||
|
||||
@@ -26,13 +26,21 @@ describe('POST /user/read-card/:cardType', () => {
|
||||
await user.update({
|
||||
'items.special.greetingReceived': [true],
|
||||
'flags.cardReceived': true,
|
||||
notifications: [{
|
||||
type: 'CARD_RECEIVED',
|
||||
data: {card: cardType},
|
||||
}],
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
|
||||
let response = await user.post(`/user/read-card/${cardType}`);
|
||||
await user.sync();
|
||||
|
||||
expect(response.message).to.equal(t('readCard', {cardType}));
|
||||
expect(user.items.special[`${cardType}Received`]).to.be.empty;
|
||||
expect(user.flags.cardReceived).to.be.false;
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /user/sleep', () => {
|
||||
let user;
|
||||
@@ -22,4 +23,15 @@ describe('POST /user/sleep', () => {
|
||||
await user.sync();
|
||||
expect(user.preferences.sleep).to.be.false;
|
||||
});
|
||||
|
||||
it('sends sleep status to analytics service', async () => {
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
await user.post('/user/sleep');
|
||||
await user.sync();
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -417,13 +417,19 @@ describe('payments/index', () => {
|
||||
it('awards mystery items when within the timeframe for a mystery item', async () => {
|
||||
let mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
let fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.notifications.find(n => n.type === 'NEW_MYSTERY_ITEMS')).to.not.be.undefined;
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(2);
|
||||
expect(user.purchased.plan.mysteryItems).to.include('armor_mystery_201605');
|
||||
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount + 1);
|
||||
expect(user.notifications[0].type).to.equal('NEW_MYSTERY_ITEMS');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('#upgradeGroupPlan', () => {
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(amzLib.authorizeOnBillingAgreement);
|
||||
amzLib.authorizeOnBillingAgreement.restore();
|
||||
uuid.v4.restore();
|
||||
});
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('subscribe', () => {
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
it('subscribes with paypal with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('checkout with subscription', () => {
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(stripe.subscriptions.update);
|
||||
stripe.subscriptions.update.restore();
|
||||
stripe.customers.create.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
@@ -144,7 +144,7 @@ describe('checkout with subscription', () => {
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
it('subscribes with stripe with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
@@ -35,7 +35,10 @@ describe('Stripe - Webhooks', () => {
|
||||
const error = new Error(`Missing handler for Stripe webhook ${eventType}`);
|
||||
await stripePayments.handleWebhooks({requestBody: event}, stripe);
|
||||
expect(logger.error).to.have.been.called.once;
|
||||
expect(logger.error).to.have.been.calledWith(error, {event: eventRetrieved});
|
||||
|
||||
const calledWith = logger.error.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(error.message);
|
||||
expect(calledWith[1].event).to.equal(eventRetrieved);
|
||||
});
|
||||
|
||||
it('retrieves and validates the event from Stripe', async () => {
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('Stripe - Upgrade Group Plan', () => {
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(stripe.subscriptions.update);
|
||||
stripe.subscriptions.update.restore();
|
||||
});
|
||||
|
||||
it('updates a group plan quantity', async () => {
|
||||
|
||||
@@ -8,7 +8,10 @@ describe('preenHistory', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// Replace system clocks so we can get predictable results
|
||||
clock = sinon.useFakeTimers(Number(moment('2013-10-20').zone(0).startOf('day').toDate()), 'Date');
|
||||
clock = sinon.useFakeTimers({
|
||||
now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()),
|
||||
toFake: ['Date'],
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
return clock.restore();
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('pushNotifications', () => {
|
||||
|
||||
sandbox.stub(nconf, 'get').returns('true-key');
|
||||
|
||||
sandbox.stub(gcmLib.Sender.prototype, 'send', fcmSendSpy);
|
||||
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
|
||||
|
||||
sandbox.stub(pushNotify, 'apn').returns({
|
||||
on: () => null,
|
||||
|
||||
@@ -24,7 +24,9 @@ describe('ensure access middlewares', () => {
|
||||
|
||||
ensureAdmin(req, res, next);
|
||||
|
||||
expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noAdminAccess')));
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(i18n.t('noAdminAccess'));
|
||||
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when user is an admin', () => {
|
||||
@@ -43,7 +45,9 @@ describe('ensure access middlewares', () => {
|
||||
|
||||
ensureSudo(req, res, next);
|
||||
|
||||
expect(next).to.be.calledWith(new NotAuthorized(apiMessages('noSudoAccess')));
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(apiMessages('noSudoAccess'));
|
||||
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when user is a sudo user', () => {
|
||||
|
||||
@@ -22,7 +22,8 @@ describe('developmentMode middleware', () => {
|
||||
|
||||
ensureDevelpmentMode(req, res, next);
|
||||
|
||||
expect(next).to.be.calledWith(new NotFound());
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when not in production', () => {
|
||||
|
||||
@@ -106,6 +106,7 @@ describe('response middleware', () => {
|
||||
type: notification.type,
|
||||
id: notification.id,
|
||||
data: {},
|
||||
seen: false,
|
||||
},
|
||||
],
|
||||
userV: res.locals.user._v,
|
||||
|
||||
@@ -1011,13 +1011,6 @@ describe('Group Model', () => {
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
'party._id': party._id,
|
||||
_id: { $ne: '' },
|
||||
}, {
|
||||
$set: {
|
||||
[`newMessages.${party._id}`]: {
|
||||
name: party.name,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1032,13 +1025,6 @@ describe('Group Model', () => {
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
guilds: group._id,
|
||||
_id: { $ne: '' },
|
||||
}, {
|
||||
$set: {
|
||||
[`newMessages.${group._id}`]: {
|
||||
name: group.name,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1049,13 +1035,6 @@ describe('Group Model', () => {
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
'party._id': party._id,
|
||||
_id: { $ne: 'user-id' },
|
||||
}, {
|
||||
$set: {
|
||||
[`newMessages.${party._id}`]: {
|
||||
name: party.name,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -58,21 +58,23 @@ describe('User Model', () => {
|
||||
|
||||
let userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('CRON');
|
||||
expect(userToJSON.notifications[0].data).to.eql({});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
});
|
||||
|
||||
it('can add notifications with data', () => {
|
||||
it('can add notifications with data and already marked as seen', () => {
|
||||
let user = new User();
|
||||
|
||||
user.addNotification('CRON', {field: 1});
|
||||
user.addNotification('CRON', {field: 1}, true);
|
||||
|
||||
let userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('CRON');
|
||||
expect(userToJSON.notifications[0].data).to.eql({field: 1});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(true);
|
||||
});
|
||||
|
||||
context('static push method', () => {
|
||||
@@ -86,7 +88,7 @@ describe('User Model', () => {
|
||||
|
||||
let userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('CRON');
|
||||
expect(userToJSON.notifications[0].data).to.eql({});
|
||||
});
|
||||
@@ -96,6 +98,7 @@ describe('User Model', () => {
|
||||
await user.save();
|
||||
|
||||
expect(User.pushNotification({_id: user._id}, 'BAD_TYPE')).to.eventually.be.rejected;
|
||||
expect(User.pushNotification({_id: user._id}, 'CRON', null, 'INVALID_SEEN')).to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it('adds notifications without data for all given users via static method', async() => {
|
||||
@@ -109,41 +112,45 @@ describe('User Model', () => {
|
||||
|
||||
let userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('CRON');
|
||||
expect(userToJSON.notifications[0].data).to.eql({});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
|
||||
user = await User.findOne({_id: otherUser._id}).exec();
|
||||
|
||||
userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('CRON');
|
||||
expect(userToJSON.notifications[0].data).to.eql({});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
});
|
||||
|
||||
it('adds notifications with data for all given users via static method', async() => {
|
||||
it('adds notifications with data and seen status for all given users via static method', async() => {
|
||||
let user = new User();
|
||||
let otherUser = new User();
|
||||
await Bluebird.all([user.save(), otherUser.save()]);
|
||||
|
||||
await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON', {field: 1});
|
||||
await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON', {field: 1}, true);
|
||||
|
||||
user = await User.findOne({_id: user._id}).exec();
|
||||
|
||||
let userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('CRON');
|
||||
expect(userToJSON.notifications[0].data).to.eql({field: 1});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(true);
|
||||
|
||||
user = await User.findOne({_id: otherUser._id}).exec();
|
||||
|
||||
userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('CRON');
|
||||
expect(userToJSON.notifications[0].data).to.eql({field: 1});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -322,6 +329,65 @@ describe('User Model', () => {
|
||||
user = await user.save();
|
||||
expect(user.achievements.beastMaster).to.not.equal(true);
|
||||
});
|
||||
|
||||
context('manage unallocated stats points notifications', () => {
|
||||
it('doesn\'t add a notification if there are no points to allocate', async () => {
|
||||
let user = new User();
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
user.stats.points = 0;
|
||||
user = await user.save();
|
||||
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount);
|
||||
});
|
||||
|
||||
it('removes a notification if there are no more points to allocate', async () => {
|
||||
let user = new User();
|
||||
user.stats.points = 9;
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
|
||||
expect(user.notifications[0].type).to.equal('UNALLOCATED_STATS_POINTS');
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
user.stats.points = 0;
|
||||
user = await user.save();
|
||||
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount - 1);
|
||||
});
|
||||
|
||||
it('adds a notification if there are points to allocate', async () => {
|
||||
let user = new User();
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
user.stats.points = 9;
|
||||
user = await user.save();
|
||||
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount + 1);
|
||||
expect(user.notifications[0].type).to.equal('UNALLOCATED_STATS_POINTS');
|
||||
expect(user.notifications[0].data.points).to.equal(9);
|
||||
});
|
||||
|
||||
it('adds a notification if the points to allocate have changed', async () => {
|
||||
let user = new User();
|
||||
user.stats.points = 9;
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
const oldNotificationsUUID = user.notifications[0].id;
|
||||
expect(user.notifications[0].type).to.equal('UNALLOCATED_STATS_POINTS');
|
||||
expect(user.notifications[0].data.points).to.equal(9);
|
||||
|
||||
user.stats.points = 11;
|
||||
user = await user.save();
|
||||
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount);
|
||||
expect(user.notifications[0].type).to.equal('UNALLOCATED_STATS_POINTS');
|
||||
expect(user.notifications[0].data.points).to.equal(11);
|
||||
expect(user.notifications[0].id).to.not.equal(oldNotificationsUUID);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('days missed', () => {
|
||||
|
||||
@@ -138,5 +138,27 @@ describe('shared.ops.buyGear', () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not buyGear equipment if user does not own prior item in sequence', (done) => {
|
||||
user.stats.gp = 200;
|
||||
|
||||
try {
|
||||
buyGear(user, {params: {key: 'armor_warrior_2'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('previousGearNotOwned'));
|
||||
expect(user.items.gear.owned).to.not.have.property('armor_warrior_2');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does buyGear equipment if item is a numbered special item user qualifies for', () => {
|
||||
user.stats.gp = 200;
|
||||
user.items.gear.owned.head_special_2 = false;
|
||||
|
||||
buyGear(user, {params: {key: 'head_special_2'}});
|
||||
|
||||
expect(user.items.gear.owned).to.have.property('head_special_2', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,11 +29,14 @@ describe('shared.ops.openMysteryItem', () => {
|
||||
let mysteryItemKey = 'eyewear_special_summerRogue';
|
||||
|
||||
user.purchased.plan.mysteryItems = [mysteryItemKey];
|
||||
user.notifications.push({type: 'NEW_MYSTERY_ITEMS', data: {items: [mysteryItemKey]}});
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
|
||||
let [data, message] = openMysteryItem(user);
|
||||
|
||||
expect(user.items.gear.owned[mysteryItemKey]).to.be.true;
|
||||
expect(message).to.equal(i18n.t('mysteryItemOpened'));
|
||||
expect(data).to.eql(content.gear.flat[mysteryItemKey]);
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,10 +39,17 @@ describe('shared.ops.readCard', () => {
|
||||
});
|
||||
|
||||
it('reads a card', () => {
|
||||
user.notifications.push({
|
||||
type: 'CARD_RECEIVED',
|
||||
data: {card: cardType},
|
||||
});
|
||||
const initialNotificationNuber = user.notifications.length;
|
||||
|
||||
let [, message] = readCard(user, {params: {cardType: 'greeting'}});
|
||||
|
||||
expect(message).to.equal(i18n.t('readCard', {cardType}));
|
||||
expect(user.items.special[`${cardType}Received`]).to.be.empty;
|
||||
expect(user.flags.cardReceived).to.be.false;
|
||||
expect(user.notifications.length).to.equal(initialNotificationNuber - 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,13 +74,6 @@ describe('shared.ops.scoreTask', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('checks that the streak parameters affects the score', () => {
|
||||
let task = generateDaily({ userId: ref.afterUser._id, text: 'task to check streak' });
|
||||
scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false });
|
||||
scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false });
|
||||
expect(task.streak).to.eql(2);
|
||||
});
|
||||
|
||||
it('completes when the task direction is up', () => {
|
||||
let task = generateTodo({ userId: ref.afterUser._id, text: 'todo to complete', cron: false });
|
||||
scoreTask({ user: ref.afterUser, task, direction: 'up' });
|
||||
@@ -123,6 +116,64 @@ describe('shared.ops.scoreTask', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('checks that the streak parameters affects the score', () => {
|
||||
let task = generateDaily({ userId: ref.afterUser._id, text: 'task to check streak' });
|
||||
scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false });
|
||||
scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false });
|
||||
expect(task.streak).to.eql(2);
|
||||
});
|
||||
|
||||
describe('verifies that 21-day streak achievements are given/removed correctly', () => {
|
||||
let initialStreakCount = 20; // 1 before the streak achievement is awarded
|
||||
beforeEach(() => {
|
||||
ref = beforeAfter();
|
||||
});
|
||||
|
||||
it('awards the first streak achievement', () => {
|
||||
let task = generateDaily({ userId: ref.afterUser._id, text: 'some daily', streak: initialStreakCount });
|
||||
scoreTask({ user: ref.afterUser, task, direction: 'up' });
|
||||
expect(ref.afterUser.achievements.streak).to.equal(1);
|
||||
});
|
||||
|
||||
it('increments the streak achievement for a second streak', () => {
|
||||
let task1 = generateDaily({ userId: ref.afterUser._id, text: 'first daily', streak: initialStreakCount });
|
||||
scoreTask({ user: ref.afterUser, task: task1, direction: 'up' });
|
||||
let task2 = generateDaily({ userId: ref.afterUser._id, text: 'second daily', streak: initialStreakCount });
|
||||
scoreTask({ user: ref.afterUser, task: task2, direction: 'up' });
|
||||
expect(ref.afterUser.achievements.streak).to.equal(2);
|
||||
});
|
||||
|
||||
it('removes the first streak achievement when unticking a Daily', () => {
|
||||
let task = generateDaily({ userId: ref.afterUser._id, text: 'some daily', streak: initialStreakCount });
|
||||
scoreTask({ user: ref.afterUser, task, direction: 'up' });
|
||||
scoreTask({ user: ref.afterUser, task, direction: 'down' });
|
||||
expect(ref.afterUser.achievements.streak).to.equal(0);
|
||||
});
|
||||
|
||||
it('decrements a multiple streak achievement when unticking a Daily', () => {
|
||||
let task1 = generateDaily({ userId: ref.afterUser._id, text: 'first daily', streak: initialStreakCount });
|
||||
scoreTask({ user: ref.afterUser, task: task1, direction: 'up' });
|
||||
let task2 = generateDaily({ userId: ref.afterUser._id, text: 'second daily', streak: initialStreakCount });
|
||||
scoreTask({ user: ref.afterUser, task: task2, direction: 'up' });
|
||||
scoreTask({ user: ref.afterUser, task: task2, direction: 'down' });
|
||||
expect(ref.afterUser.achievements.streak).to.equal(1);
|
||||
});
|
||||
|
||||
it('does not give a streak achievement for a streak of zero', () => {
|
||||
let task = generateDaily({ userId: ref.afterUser._id, text: 'some daily', streak: -1 });
|
||||
scoreTask({ user: ref.afterUser, task, direction: 'up' });
|
||||
expect(ref.afterUser.achievements.streak).to.be.undefined;
|
||||
});
|
||||
|
||||
it('does not remove a streak achievement when unticking a Daily gives a streak of zero', () => {
|
||||
let task1 = generateDaily({ userId: ref.afterUser._id, text: 'first daily', streak: initialStreakCount });
|
||||
scoreTask({ user: ref.afterUser, task: task1, direction: 'up' });
|
||||
let task2 = generateDaily({ userId: ref.afterUser._id, text: 'second daily', streak: 1 });
|
||||
scoreTask({ user: ref.afterUser, task: task2, direction: 'down' });
|
||||
expect(ref.afterUser.achievements.streak).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scores', () => {
|
||||
let options = {};
|
||||
let habit;
|
||||
|
||||
@@ -137,3 +137,9 @@
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,15 @@
|
||||
background-color: rgba(#d5c8ff, 0.32);
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
&.dropdown-inactive {
|
||||
cursor: default;
|
||||
|
||||
&:active, &:hover, &.active {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.icon-12 {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.icon-10 {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
.badge-svg {
|
||||
left: calc((100% - 18px) / 2);
|
||||
cursor: pointer;
|
||||
color: $gray-400;
|
||||
background: $white;
|
||||
padding: 4.5px 6px;
|
||||
left: calc((100% - 18px) / 2);
|
||||
cursor: pointer;
|
||||
color: $gray-400;
|
||||
background: $white;
|
||||
padding: 4.5px 6px;
|
||||
|
||||
&.item-selected-badge {
|
||||
background: $purple-300;
|
||||
color: $white;
|
||||
}
|
||||
&.item-selected-badge {
|
||||
background: $purple-300;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
span.badge.badge-pill.badge-item.badge-svg:not(.item-selected-badge) {
|
||||
color: #a5a1ac;
|
||||
}
|
||||
span.badge.badge-pill.badge-item.badge-svg:not(.item-selected-badge) {
|
||||
color: #a5a1ac;
|
||||
}
|
||||
|
||||
span.badge.badge-pill.badge-item.badge-svg.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
span.badge.badge-pill.badge-item.badge-svg.hide {
|
||||
display: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
span.badge.badge-pill.badge-item.badge-svg.hide {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-12 {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,32 @@
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.container-fluid {
|
||||
.container-fluid.static-view {
|
||||
margin: 5em 2em 0 2em;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
margin-top: 0.5em;
|
||||
color: $purple-200;
|
||||
}
|
||||
.static-view {
|
||||
h1, h2 {
|
||||
margin-top: 0.5em;
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
h3, h4 {
|
||||
color: $purple-200;
|
||||
}
|
||||
h3, h4 {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
li, p {
|
||||
font-size: 16px;
|
||||
}
|
||||
li, p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.media img {
|
||||
margin: 1em;
|
||||
}
|
||||
.media img {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.center-block {
|
||||
margin: 0 auto 1em auto;
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,27 @@ body {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
a {
|
||||
a, a:not([href]):not([tabindex]) {
|
||||
cursor: pointer;
|
||||
|
||||
&.standard-link {
|
||||
color: $blue-10;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&[disabled="disabled"] {
|
||||
color: $gray-300;
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&.small-link {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
}
|
||||
}
|
||||
|
||||
// Headers
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="23" height="28" viewBox="0 0 23 28">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#50B5E9" d="M11.074 8.485l4.26-2.121-4.26-2.122L8.944 0l-2.13 4.242-4.258 2.122 4.259 2.12 2.13 4.243z"/>
|
||||
<path fill="#9A62FF" d="M5.111 19.09l2.556-1.272-2.556-1.273L3.833 14l-1.277 2.545L0 17.818l2.556 1.273 1.277 2.545z"/>
|
||||
<path fill="#FF6165" d="M12.778 25.455l2.555-1.273-2.555-1.273-1.278-2.545-1.278 2.545-2.555 1.273 2.555 1.273L11.5 28z"/>
|
||||
<path fill="#FFB445" d="M19.593 15.697L23 14l-3.407-1.697-1.704-3.394-1.704 3.394L12.778 14l3.407 1.697 1.704 3.394z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 653 B |
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="104" viewBox="0 0 256 104">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g opacity=".64" transform="translate(-44 -16)">
|
||||
<rect width="346" height="136" rx="2"/>
|
||||
<path fill="#3FDAA2" d="M232.359 84.179l3.074-1.341-2.917-1.655-1.341-3.075-1.655 2.918-3.075 1.34 2.918 1.656 1.34 3.074z" opacity=".96"/>
|
||||
<path fill="#FF6165" d="M171.439 20.105l-.023 2.982 2.399-1.771 2.981.023-1.77-2.4.022-2.98-2.399 1.77-2.98-.022z" opacity=".84"/>
|
||||
<path fill="#3FDAA2" d="M114.501 54.126l2.574.428-1.202-2.315.428-2.574-2.316 1.202-2.573-.427 1.202 2.315-.428 2.573z" opacity=".83"/>
|
||||
<path fill="#50B5E9" d="M284.929 89.34l.173 6.333 4.962-3.939 6.334-.173-3.939-4.962-.173-6.334-4.962 3.939-6.334.173z" opacity=".73"/>
|
||||
<path fill="#FFBE5D" d="M242.881 57.724l-3.984 5.397 6.708-.05 5.397 3.983-.05-6.708 3.983-5.397-6.708.051-5.397-3.984z" opacity=".82"/>
|
||||
<path fill="#50B5E9" d="M125.165 110.829l-.589-4.433-3.193 3.13-4.433.59 3.13 3.193.59 4.433 3.193-3.13 4.433-.59z" opacity=".99"/>
|
||||
<path fill="#50B5E9" d="M163.702 56.186l-3.901 5.91 7.068-.425 5.91 3.902-.425-7.069 3.901-5.909-7.068.425-5.909-3.902z"/>
|
||||
<path fill="#FF6165" d="M206.14 107.367l3.404 2.9.278-4.463 2.9-3.404-4.463-.278-3.404-2.9-.278 4.463-2.9 3.404z" opacity=".84"/>
|
||||
<path fill="#FF944C" d="M50.708 53.066l.62 3.675 2.568-2.7 3.675-.62-2.7-2.568-.62-3.675-2.568 2.7-3.675.62zM297.486 59.18l-.037-3.727-2.96 2.265-3.726.037 2.265 2.959.037 3.727 2.96-2.266 3.726-.037z" opacity=".93"/>
|
||||
<path fill="#9A62FF" d="M95.481 86.952l5.13-.957-3.843-3.53-.957-5.128-3.53 3.842-5.128.957 3.843 3.53.956 5.128z" opacity=".99"/>
|
||||
<path fill="#FFBE5D" d="M122.061 24.656l-.952 3.987 3.761-1.63 3.987.952-1.63-3.761.953-3.988-3.762 1.63-3.987-.952z"/>
|
||||
<path fill="#3FDAA2" d="M49.863 86.74l4.692 1.207-1.849-4.478 1.208-4.692-4.478 1.849-4.692-1.208 1.849 4.478-1.208 4.692z" opacity=".96"/>
|
||||
<path fill="#9A62FF" d="M226.63 30.447l5.026-1.4-4.135-3.18-1.4-5.027-3.181 4.136-5.026 1.4 4.135 3.18 1.4 5.027z" opacity=".99"/>
|
||||
</g>
|
||||
<path fill="#3FDAA2" d="M119.347 104c-2.031 0-3.971-.8-5.41-2.24L89 76.824l10.819-10.818 19.098 19.098L157.57 40l11.619 9.953-44.03 51.378a7.694 7.694 0 0 1-5.52 2.662c-.097.007-.195.007-.292.007"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -21,6 +21,7 @@
|
||||
:hideClassBadge='true',
|
||||
:spritesMargin='"1.8em 1.5em"',
|
||||
:overrideTopPadding='"0px"',
|
||||
:showVisualBuffs='false',
|
||||
:class='selectionBox(selectedClass, heroClass)',
|
||||
)
|
||||
br
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
| {{ $t('contribModal', {name: user.profile.name, level: user.contributor.level}) }}
|
||||
br
|
||||
a(:href="$t('conRewardsURL')", target='_blank') {{ $t('contribLink') }}
|
||||
br
|
||||
button.btn.btn-primary(style='margin-top:1em' @click='close()') {{ $t('huzzah') }}
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
<template lang="pug">
|
||||
b-modal#new-stuff(
|
||||
v-if='user.flags.newStuff',
|
||||
size='lg',
|
||||
:hide-header='true',
|
||||
:hide-footer='true',
|
||||
)
|
||||
.modal-body
|
||||
new-stuff
|
||||
.static-view(v-html='html')
|
||||
.modal-footer
|
||||
a.btn.btn-info(href='http://habitica.wikia.com/wiki/Whats_New', target='_blank') {{ this.$t('newsArchive') }}
|
||||
button.btn.btn-secondary(@click='close()') {{ this.$t('cool') }}
|
||||
button.btn.btn-secondary(@click='tellMeLater()') {{ this.$t('tellMeLater') }}
|
||||
button.btn.btn-warning(@click='dismissAlert();') {{ this.$t('dismissAlert') }}
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/static.scss';
|
||||
<style lang='scss'>
|
||||
@import '~client/assets/scss/static.scss';
|
||||
</style>
|
||||
|
||||
.modal-body {
|
||||
padding-top: 2em;
|
||||
}
|
||||
<style lang='scss' scoped>
|
||||
.modal-body {
|
||||
padding-top: 2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import markdown from 'client/directives/markdown';
|
||||
import newStuff from 'client/components/static/newStuff';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
newStuff,
|
||||
data () {
|
||||
return {
|
||||
html: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
directives: {
|
||||
markdown,
|
||||
},
|
||||
mounted () {
|
||||
async mounted () {
|
||||
this.$root.$on('bv::show::modal', async (modalId) => {
|
||||
if (modalId !== 'new-stuff') return;
|
||||
// Request the lastest news, but not locally incase they don't refresh
|
||||
// let response = await axios.get('/static/new-stuff');
|
||||
let response = await axios.get('/api/v3/news');
|
||||
this.html = response.data.html;
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('bv::show::modal');
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
tellMeLater () {
|
||||
this.$store.dispatch('user:newStuffLater');
|
||||
this.$root.$emit('bv::hide::modal', 'new-stuff');
|
||||
},
|
||||
dismissAlert () {
|
||||
this.$store.dispatch('user:set', {'flags.newStuff': false});
|
||||
this.close();
|
||||
this.$root.$emit('bv::hide::modal', 'new-stuff');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
h3
|
||||
a(href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica', target='_blank') {{ $t('mobileAndroid') }}
|
||||
.col-12.col-md-2
|
||||
h3 Company
|
||||
h3 {{ $t('footerCompany') }}
|
||||
ul
|
||||
li
|
||||
router-link(to='/static/features') {{ $t('companyAbout') }}
|
||||
@@ -29,7 +29,7 @@
|
||||
li
|
||||
router-link(to='/static/contact') {{ $t('contactUs') }}
|
||||
.col-12.col-md-2
|
||||
h3 Community
|
||||
h3 {{ $t('footerCommunity') }}
|
||||
ul
|
||||
li
|
||||
a(target="_blanck", href="/static/community-guidelines") {{ $t('communityGuidelines') }}
|
||||
@@ -50,7 +50,7 @@
|
||||
.col-12.col-md-6
|
||||
.row
|
||||
.col-6
|
||||
h3 Developers
|
||||
h3 {{ $t('footerDevs') }}
|
||||
ul
|
||||
li
|
||||
a(href='/apidoc', target='_blank') {{ $t('APIv3') }}
|
||||
@@ -83,25 +83,25 @@
|
||||
hr
|
||||
.row
|
||||
.col-12.col-md-5
|
||||
| © 2017 Habitica. All rights reserved.
|
||||
| © 2018 Habitica. All rights reserved.
|
||||
.debug.float-left(v-if="!IS_PRODUCTION && isUserLoaded")
|
||||
button.btn.btn-primary(@click="debugMenuShown = !debugMenuShown") Toggle Debug Menu
|
||||
.debug-group(v-if="debugMenuShown")
|
||||
a.btn.btn-default(@click="setHealthLow()") Health = 1
|
||||
a.btn.btn-default(@click="addMissedDay(1)") +1 Missed Day
|
||||
a.btn.btn-default(@click="addMissedDay(2)") +2 Missed Days
|
||||
a.btn.btn-default(@click="addMissedDay(8)") +8 Missed Days
|
||||
a.btn.btn-default(@click="addMissedDay(32)") +32 Missed Days
|
||||
a.btn.btn-default(@click="addTenGems()") +10 Gems
|
||||
a.btn.btn-default(@click="addHourglass()") +1 Mystic Hourglass
|
||||
a.btn.btn-default(@click="addGold()") +500GP
|
||||
a.btn.btn-default(@click="plusTenHealth()") + 10HP
|
||||
a.btn.btn-default(@click="addMana()") +MP
|
||||
a.btn.btn-default(@click="addLevelsAndGold()") +Exp +GP +MP
|
||||
a.btn.btn-default(@click="addOneLevel()") +1 Level
|
||||
a.btn.btn-default(@click="addQuestProgress()", tooltip="+1000 to boss quests. 300 items to collection quests") Quest Progress Up
|
||||
a.btn.btn-default(@click="makeAdmin()") Make Admin
|
||||
a.btn.btn-default(@click="openModifyInventoryModal()") Modify Inventory
|
||||
a.btn.btn-secondary(@click="setHealthLow()") Health = 1
|
||||
a.btn.btn-secondary(@click="addMissedDay(1)") +1 Missed Day
|
||||
a.btn.btn-secondary(@click="addMissedDay(2)") +2 Missed Days
|
||||
a.btn.btn-secondary(@click="addMissedDay(8)") +8 Missed Days
|
||||
a.btn.btn-secondary(@click="addMissedDay(32)") +32 Missed Days
|
||||
a.btn.btn-secondary(@click="addTenGems()") +10 Gems
|
||||
a.btn.btn-secondary(@click="addHourglass()") +1 Mystic Hourglass
|
||||
a.btn.btn-secondary(@click="addGold()") +500GP
|
||||
a.btn.btn-secondary(@click="plusTenHealth()") + 10HP
|
||||
a.btn.btn-secondary(@click="addMana()") +MP
|
||||
a.btn.btn-secondary(@click="addLevelsAndGold()") +Exp +GP +MP
|
||||
a.btn.btn-secondary(@click="addOneLevel()") +1 Level
|
||||
a.btn.btn-secondary(@click="addQuestProgress()", tooltip="+1000 to boss quests. 300 items to collection quests") Quest Progress Up
|
||||
a.btn.btn-secondary(@click="makeAdmin()") Make Admin
|
||||
a.btn.btn-secondary(@click="openModifyInventoryModal()") Modify Inventory
|
||||
.col-12.col-md-2.text-center
|
||||
.logo.svg-icon(v-html='icons.gryphon')
|
||||
.col-12.col-md-5.text-right
|
||||
@@ -149,11 +149,6 @@
|
||||
color: #c3c0c7;
|
||||
}
|
||||
}
|
||||
|
||||
& > .row {
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
.row
|
||||
challenge-modal(:cloning='cloning' v-on:updatedChallenge='updatedChallenge')
|
||||
challenge-modal(v-on:updatedChallenge='updatedChallenge')
|
||||
leave-challenge-modal(:challengeId='challenge._id')
|
||||
close-challenge-modal(:members='members', :challengeId='challenge._id')
|
||||
challenge-member-progress-modal(:memberId='progressMemberId', :challengeId='challenge._id')
|
||||
@@ -220,7 +220,6 @@ export default {
|
||||
memberIcon,
|
||||
calendarIcon,
|
||||
}),
|
||||
cloning: false,
|
||||
challenge: {},
|
||||
members: [],
|
||||
tasksByType: {
|
||||
@@ -261,10 +260,6 @@ export default {
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
this.searchId = to.params.challengeId;
|
||||
await this.loadChallenge();
|
||||
|
||||
if (this.$store.state.challengeOptions.cloning) {
|
||||
this.cloneTasks(this.$store.state.challengeOptions.tasksToClone);
|
||||
}
|
||||
next();
|
||||
},
|
||||
methods: {
|
||||
@@ -284,28 +279,6 @@ export default {
|
||||
|
||||
return cleansedTask;
|
||||
},
|
||||
cloneTasks (tasksToClone) {
|
||||
let clonedTasks = [];
|
||||
|
||||
for (let key in tasksToClone) {
|
||||
let tasksSection = tasksToClone[key];
|
||||
tasksSection.forEach(task => {
|
||||
let clonedTask = cloneDeep(task);
|
||||
clonedTask = this.cleanUpTask(clonedTask);
|
||||
clonedTask = taskDefaults(clonedTask);
|
||||
this.tasksByType[task.type].push(clonedTask);
|
||||
clonedTasks.push(clonedTask);
|
||||
});
|
||||
}
|
||||
|
||||
this.$store.dispatch('tasks:createChallengeTasks', {
|
||||
challengeId: this.searchId,
|
||||
tasks: clonedTasks,
|
||||
});
|
||||
|
||||
this.$store.state.challengeOptions.cloning = false;
|
||||
this.$store.state.challengeOptions.tasksToClone = [];
|
||||
},
|
||||
async loadChallenge () {
|
||||
this.challenge = await this.$store.dispatch('challenges:getChallenge', {challengeId: this.searchId});
|
||||
this.members = await this.$store.dispatch('members:getChallengeMembers', {challengeId: this.searchId});
|
||||
@@ -377,7 +350,6 @@ export default {
|
||||
},
|
||||
edit () {
|
||||
// @TODO: set working challenge
|
||||
this.cloning = false;
|
||||
this.$store.state.challengeOptions.workingChallenge = Object.assign({}, this.$store.state.challengeOptions.workingChallenge, this.challenge);
|
||||
this.$root.$emit('bv::show::modal', 'challenge-modal');
|
||||
},
|
||||
@@ -396,10 +368,9 @@ export default {
|
||||
window.location = `/api/v3/challenges/${this.searchId}/export/csv`;
|
||||
},
|
||||
cloneChallenge () {
|
||||
this.cloning = true;
|
||||
this.$store.state.challengeOptions.tasksToClone = this.tasksByType;
|
||||
this.$store.state.challengeOptions.workingChallenge = Object.assign({}, this.$store.state.challengeOptions.workingChallenge, this.challenge);
|
||||
this.$root.$emit('bv::show::modal', 'challenge-modal');
|
||||
this.$root.$emit('habitica:clone-challenge', {
|
||||
challenge: this.challenge,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,10 +53,9 @@
|
||||
input(type='number', :min='minPrize', :max='maxPrize', v-model="workingChallenge.prize")
|
||||
.row.footer-wrap
|
||||
.col-12.text-center.submit-button-wrapper
|
||||
.alert.alert-warning(v-if='insufficientGemsForTavernChallenge')
|
||||
You do not have enough gems to create a Tavern challenge
|
||||
// @TODO if buy gems button is added, add analytics tracking to it
|
||||
// see https://github.com/HabitRPG/habitica/blob/develop/website/views/options/social/challenges.jade#L134
|
||||
.alert.alert-warning(v-if='insufficientGemsForTavernChallenge') You do not have enough gems to create a Tavern challenge
|
||||
// @TODO if buy gems button is added, add analytics tracking to it
|
||||
// see https://github.com/HabitRPG/habitica/blob/develop/website/views/options/social/challenges.jade#L134
|
||||
button.btn.btn-primary(v-if='creating && !cloning', @click='createChallenge()', :disabled='loading') {{$t('createChallengeAddTasks')}}
|
||||
button.btn.btn-primary(v-once, v-if='cloning', @click='createChallenge()', :disabled='loading') {{$t('createChallengeCloneTasks')}}
|
||||
button.btn.btn-primary(v-once, v-if='!creating && !cloning', @click='updateChallenge()') {{$t('updateChallenge')}}
|
||||
@@ -139,7 +138,7 @@ import { TAVERN_ID, MIN_SHORTNAME_SIZE_FOR_CHALLENGES, MAX_SUMMARY_SIZE_FOR_CHAL
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['groupId', 'cloning'],
|
||||
props: ['groupId'],
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
@@ -225,6 +224,8 @@ export default {
|
||||
shortName: '',
|
||||
todos: [],
|
||||
},
|
||||
cloning: false,
|
||||
cloningChallengeId: '',
|
||||
showCategorySelect: false,
|
||||
categoryOptions,
|
||||
categoriesHashByKey,
|
||||
@@ -232,6 +233,18 @@ export default {
|
||||
groups: [],
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:clone-challenge', (data) => {
|
||||
if (!data.challenge) return;
|
||||
this.cloning = true;
|
||||
this.cloningChallengeId = data.challenge._id;
|
||||
this.$store.state.challengeOptions.workingChallenge = Object.assign({}, this.$store.state.challengeOptions.workingChallenge, data.challenge);
|
||||
this.$root.$emit('bv::show::modal', 'challenge-modal');
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:clone-challenge');
|
||||
},
|
||||
watch: {
|
||||
user () {
|
||||
if (!this.challenge) this.workingChallenge.leader = this.user._id;
|
||||
@@ -252,7 +265,6 @@ export default {
|
||||
if (this.creating) {
|
||||
return this.$t('createChallenge');
|
||||
}
|
||||
|
||||
return this.$t('editingChallenge');
|
||||
},
|
||||
charactersRemaining () {
|
||||
@@ -322,6 +334,8 @@ export default {
|
||||
if (!this.challenge) return;
|
||||
|
||||
this.workingChallenge = Object.assign({}, this.workingChallenge, this.challenge);
|
||||
// @TODO: Should we use a separate field? I think the API expects `group` but it is confusing
|
||||
this.workingChallenge.group = this.workingChallenge.group._id;
|
||||
this.workingChallenge.categories = [];
|
||||
|
||||
if (this.challenge.categories) {
|
||||
@@ -388,15 +402,40 @@ export default {
|
||||
let challengeDetails = clone(this.workingChallenge);
|
||||
challengeDetails.categories = serverCategories;
|
||||
|
||||
let challenge = await this.$store.dispatch('challenges:createChallenge', {challenge: challengeDetails});
|
||||
// @TODO: When to remove from guild instead?
|
||||
this.user.balance -= this.workingChallenge.prize / 4;
|
||||
let challenge;
|
||||
if (this.cloning) {
|
||||
challenge = await this.$store.dispatch('challenges:cloneChallenge', {
|
||||
challenge: challengeDetails,
|
||||
cloningChallengeId: this.cloningChallengeId,
|
||||
});
|
||||
this.cloningChallengeId = '';
|
||||
} else {
|
||||
challenge = await this.$store.dispatch('challenges:createChallenge', {challenge: challengeDetails});
|
||||
}
|
||||
|
||||
// Update Group Prize
|
||||
let challengeGroup = this.groups.find(group => {
|
||||
return group._id === this.workingChallenge.group;
|
||||
});
|
||||
|
||||
// @TODO: Share with server
|
||||
const prizeCost = this.workingChallenge.prize / 4;
|
||||
const challengeGroupLeader = challengeGroup.leader && challengeGroup.leader._id ? challengeGroup.leader._id : challengeGroup.leader;
|
||||
const userIsLeader = challengeGroupLeader === this.user._id;
|
||||
if (challengeGroup && userIsLeader && challengeGroup.balance > 0 && challengeGroup.balance >= prizeCost) {
|
||||
// Group pays for all of prize
|
||||
} else if (challengeGroup && userIsLeader && challengeGroup.balance > 0) {
|
||||
// User pays remainder of prize cost after group
|
||||
let remainder = prizeCost - challengeGroup.balance;
|
||||
this.user.balance -= remainder;
|
||||
} else {
|
||||
// User pays for all of prize
|
||||
this.user.balance -= prizeCost;
|
||||
}
|
||||
|
||||
this.$emit('createChallenge', challenge);
|
||||
this.resetWorkingChallenge();
|
||||
|
||||
if (this.cloning) this.$store.state.challengeOptions.cloning = true;
|
||||
|
||||
this.$root.$emit('bv::hide::modal', 'challenge-modal');
|
||||
this.$router.push(`/challenges/${challenge._id}`);
|
||||
},
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
.modal-body
|
||||
h2 {{ $t('confirmKeepChallengeTasks') }}
|
||||
div
|
||||
button.btn.btn-primary(@click='leaveChallenge("keep")') {{ $t('keepIt') }}
|
||||
button.btn.btn-danger(@click='leaveChallenge("remove-all")') {{ $t('removeIt') }}
|
||||
button.btn.btn-primary(@click='leaveChallenge("keep")') {{ $t('keepThem') }}
|
||||
button.btn.btn-danger(@click='leaveChallenge("remove-all")') {{ $t('removeThem') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -159,7 +159,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.col-12.customize-options
|
||||
.head_0.option(@click='set({"preferences.hair.bangs": 0})',
|
||||
:class="[{ active: user.preferences.hair.bangs === 0 }, 'hair_bangs_0_' + user.preferences.hair.color]")
|
||||
.option(v-for='option in ["1", "2", "3", "4"]',
|
||||
.option(v-for='option in [1, 2, 3, 4]',
|
||||
:class='{active: user.preferences.hair.bangs === option}')
|
||||
.bangs.sprite.customize-option(:class="`hair_bangs_${option}_${user.preferences.hair.color}`", @click='set({"preferences.hair.bangs": option})')
|
||||
#facialhair.row(v-if='activeSubPage === "facialhair"')
|
||||
@@ -227,7 +227,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
#flowers.row(v-if='activeSubPage === "flower"')
|
||||
.col-12.customize-options
|
||||
.head_0.option(@click='set({"preferences.hair.flower":0})', :class='{active: user.preferences.hair.flower === 0}')
|
||||
.option(v-for='option in ["1", "2", "3", "4", "5", "6"]',
|
||||
.option(v-for='option in [1, 2, 3, 4, 5, 6]',
|
||||
:class='{active: user.preferences.hair.flower === option}')
|
||||
.sprite.customize-option(:class="`hair_flower_${option}`", @click='set({"preferences.hair.flower": option})')
|
||||
.row(v-if='activeSubPage === "flower"')
|
||||
@@ -975,12 +975,12 @@ export default {
|
||||
rainbowSkinKeys: ['eb052b', 'f69922', 'f5d70f', '0ff591', '2b43f6', 'd7a9f7', '800ed0', 'rainbow'],
|
||||
animalSkinKeys: ['bear', 'cactus', 'fox', 'lion', 'panda', 'pig', 'tiger', 'wolf'],
|
||||
premiumHairColorKeys: ['rainbow', 'yellow', 'green', 'purple', 'blue', 'TRUred'],
|
||||
baseHair1: ['1', '3'],
|
||||
baseHair2Keys: ['2', '4', '5', '6', '7', '8'],
|
||||
baseHair3Keys: ['9', '10', '11', '12', '13', '14'],
|
||||
baseHair4Keys: ['15', '16', '17', '18', '19', '20'],
|
||||
baseHair5Keys: ['1', '2', '3'],
|
||||
baseHair6Keys: ['1', '2'],
|
||||
baseHair1: [1, 3],
|
||||
baseHair2Keys: [2, 4, 5, 6, 7, 8],
|
||||
baseHair3Keys: [9, 10, 11, 12, 13, 14],
|
||||
baseHair4Keys: [15, 16, 17, 18, 19, 20],
|
||||
baseHair5Keys: [1, 2, 3],
|
||||
baseHair6Keys: [1, 2],
|
||||
animalEarsKeys: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
|
||||
icons: Object.freeze({
|
||||
logoPurple,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<template lang="pug">
|
||||
.row.community-guidelines(v-if='!communityGuidelinesAccepted')
|
||||
div.col.col-sm-12.col-xl-8(v-once, v-html="$t('communityGuidelinesIntro')")
|
||||
|
||||
div.col-md-auto.col-md-12.col-xl-4
|
||||
button.btn.btn-info.btn-follow-guidelines(@click='acceptCommunityGuidelines()', v-once) {{ $t('acceptCommunityGuidelines') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.community-guidelines {
|
||||
background-color: rgba(135, 129, 144, 0.84);
|
||||
padding: 1em;
|
||||
color: $white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 150px;
|
||||
margin-top: 2.3em;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.btn-follow-guidelines {
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
communityGuidelinesAccepted () {
|
||||
return this.user.flags.communityGuidelinesAccepted;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
acceptCommunityGuidelines () {
|
||||
this.$store.dispatch('user:set', {'flags.communityGuidelinesAccepted': true});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -19,7 +19,7 @@
|
||||
.svg-icon.shield(v-html="icons.silverGuildBadgeIcon", v-if='group.memberCount > 100 && group.memberCount < 999')
|
||||
.svg-icon.shield(v-html="icons.bronzeGuildBadgeIcon", v-if='group.memberCount < 100')
|
||||
span.number {{ group.memberCount | abbrNum }}
|
||||
div(v-once) {{ $t('memberList') }}
|
||||
div.member-list(v-once) {{ $t('memberList') }}
|
||||
.col-4(v-if='!isParty')
|
||||
.item-with-icon(@click='showGroupGems()')
|
||||
.svg-icon.gem(v-html="icons.gem")
|
||||
@@ -31,23 +31,19 @@
|
||||
.row.new-message-row
|
||||
textarea(:placeholder="!isParty ? $t('chatPlaceholder') : $t('partyChatPlaceholder')", v-model='newMessage', @keydown='updateCarretPosition', @keyup.ctrl.enter='sendMessage()')
|
||||
autocomplete(:text='newMessage', v-on:select="selectedAutocomplete", :coords='coords', :chat='group.chat')
|
||||
.row
|
||||
.col-6
|
||||
.row.chat-actions
|
||||
.col-6.chat-receive-actions
|
||||
button.btn.btn-secondary.float-left.fetch(v-once, @click='fetchRecentMessages()') {{ $t('fetchRecentMessages') }}
|
||||
button.btn.btn-secondary.float-left(v-once, @click='reverseChat()') {{ $t('reverseChat') }}
|
||||
.col-6
|
||||
.col-6.chat-send-actions
|
||||
button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }}
|
||||
.row.community-guidelines(v-if='!communityGuidelinesAccepted')
|
||||
div.col-8(v-once, v-html="$t('communityGuidelinesIntro')")
|
||||
div.col-4
|
||||
button.btn.btn-info(@click='acceptCommunityGuidelines()', v-once) {{ $t('acceptCommunityGuidelines') }}
|
||||
community-guidelines
|
||||
.row
|
||||
.col-12.hr
|
||||
chat-message(:chat.sync='group.chat', :group-id='group._id', group-name='group.name')
|
||||
.col-12.col-sm-4.sidebar
|
||||
.row(:class='{"guild-background": !isParty}')
|
||||
.col-6
|
||||
.col-6
|
||||
.col-12
|
||||
.button-container
|
||||
button.btn.btn-success(class='btn-success', v-if='isLeader && !group.purchased.active', @click='upgradeGroup()')
|
||||
| {{ $t('upgrade') }}
|
||||
@@ -105,7 +101,7 @@
|
||||
.section(v-if="sections.challenges")
|
||||
group-challenges(:groupId='searchId')
|
||||
div.text-center
|
||||
button.btn.btn-danger(v-if='isMember', @click='clickLeave()') {{ $t('leave') }}
|
||||
button.btn.btn-danger(v-if='isMember', @click='clickLeave()') {{ isParty ? $t('leaveParty') : $t('leaveGroup') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -146,7 +142,7 @@
|
||||
|
||||
.svg-icon.shield, .svg-icon.gem {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
@@ -157,6 +153,10 @@
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
margin-top: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.item-with-icon:hover {
|
||||
@@ -166,6 +166,7 @@
|
||||
.sidebar {
|
||||
background-color: $gray-600;
|
||||
padding-bottom: 2em;
|
||||
padding-top: 2.8em;
|
||||
}
|
||||
|
||||
.card {
|
||||
@@ -229,22 +230,29 @@
|
||||
.chat-row {
|
||||
margin-top: 2em;
|
||||
|
||||
.community-guidelines {
|
||||
background-color: rgba(135, 129, 144, 0.84);
|
||||
padding: 1em;
|
||||
color: $white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 150px;
|
||||
padding-top: 3em;
|
||||
margin-top: 2.3em;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.new-message-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-actions {
|
||||
margin-top: 1em;
|
||||
|
||||
.chat-receive-actions {
|
||||
padding-left: 0;
|
||||
|
||||
button {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-send-actions {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span.action {
|
||||
@@ -311,6 +319,7 @@ import groupChallenges from '../challenges/groupChallenges';
|
||||
import groupGemsModal from 'client/components/groups/groupGemsModal';
|
||||
import questSidebarSection from 'client/components/groups/questSidebarSection';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import communityGuidelines from './communityGuidelines';
|
||||
|
||||
import deleteIcon from 'assets/svg/delete.svg';
|
||||
import copyIcon from 'assets/svg/copy.svg';
|
||||
@@ -341,6 +350,7 @@ export default {
|
||||
questDetailsModal,
|
||||
groupGemsModal,
|
||||
questSidebarSection,
|
||||
communityGuidelines,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
@@ -381,9 +391,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
communityGuidelinesAccepted () {
|
||||
return this.user.flags.communityGuidelinesAccepted;
|
||||
},
|
||||
partyStore () {
|
||||
return this.$store.state.party;
|
||||
},
|
||||
@@ -420,14 +427,9 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (this.isParty) this.searchId = 'party';
|
||||
if (!this.searchId) this.searchId = this.groupId;
|
||||
|
||||
this.load();
|
||||
|
||||
if (this.user.newMessages[this.searchId]) {
|
||||
this.$store.dispatch('chat:markChatSeen', {groupId: this.searchId});
|
||||
this.$delete(this.user.newMessages, this.searchId);
|
||||
}
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.$set(this, 'searchId', to.params.groupId);
|
||||
@@ -454,16 +456,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
acceptCommunityGuidelines () {
|
||||
this.$store.dispatch('user:set', {'flags.communityGuidelinesAccepted': true});
|
||||
},
|
||||
load () {
|
||||
if (this.isParty) {
|
||||
this.searchId = 'party';
|
||||
// @TODO: Set up from old client. Decide what we need and what we don't
|
||||
// Check Desktop notifs
|
||||
// Load invites
|
||||
}
|
||||
this.fetchGuild();
|
||||
|
||||
this.$root.$on('updatedGroup', group => {
|
||||
@@ -546,6 +539,22 @@ export default {
|
||||
const group = await this.$store.dispatch('guilds:getGroup', {groupId: this.searchId});
|
||||
this.$set(this, 'group', group);
|
||||
}
|
||||
|
||||
const groupId = this.searchId === 'party' ? this.user.party._id : this.searchId;
|
||||
if (this.hasUnreadMessages(groupId)) {
|
||||
// Delay by 1sec to make sure it returns after other requests that don't have the notification marked as read
|
||||
setTimeout(() => {
|
||||
this.$store.dispatch('chat:markChatSeen', {groupId});
|
||||
this.$delete(this.user.newMessages, groupId);
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
hasUnreadMessages (groupId) {
|
||||
if (this.user.newMessages[groupId]) return true;
|
||||
|
||||
return this.user.notifications.some(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId;
|
||||
});
|
||||
},
|
||||
deleteAllMessages () {
|
||||
if (confirm(this.$t('confirmDeleteAllMessages'))) {
|
||||
@@ -571,8 +580,7 @@ export default {
|
||||
if (this.group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
|
||||
return;
|
||||
}
|
||||
await this.$store.dispatch('guilds:join', {guildId: this.group._id, type: 'myGuilds'});
|
||||
this.user.guilds.push(this.group._id);
|
||||
await this.$store.dispatch('guilds:join', {groupId: this.group._id, type: 'guild'});
|
||||
},
|
||||
clickLeave () {
|
||||
Analytics.track({
|
||||
@@ -612,20 +620,6 @@ export default {
|
||||
this.$store.state.upgradingGroup = this.group;
|
||||
this.$router.push('/group-plans');
|
||||
},
|
||||
// @TODO: Move to notificatin component
|
||||
async leaveOldPartyAndJoinNewParty () {
|
||||
let newPartyName = 'where does this come from';
|
||||
if (!confirm(`Are you sure you want to delete your party and join${newPartyName}?`)) return;
|
||||
|
||||
let keepChallenges = 'remain-in-challenges';
|
||||
await this.$store.dispatch('guilds:leave', {
|
||||
groupId: this.group._id,
|
||||
keep: false,
|
||||
keepChallenges,
|
||||
});
|
||||
|
||||
await this.$store.dispatch('guilds:join', {groupId: this.group._id});
|
||||
},
|
||||
clickStartQuest () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
|
||||
@@ -130,7 +130,6 @@ import moment from 'moment';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import groupUtilities from 'client/mixins/groupsUtilities';
|
||||
import markdown from 'client/directives/markdown';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import goldGuildBadgeIcon from 'assets/svg/gold-guild-badge-large.svg';
|
||||
import silverGuildBadgeIcon from 'assets/svg/silver-guild-badge-large.svg';
|
||||
@@ -171,20 +170,12 @@ export default {
|
||||
if (this.guild.cancelledPlan && !confirm(window.env.t('aboutToJoinCancelledGroupPlan'))) {
|
||||
return;
|
||||
}
|
||||
await this.$store.dispatch('guilds:join', {guildId: this.guild._id, type: 'myGuilds'});
|
||||
await this.$store.dispatch('guilds:join', {groupId: this.guild._id, type: 'guild'});
|
||||
},
|
||||
async leave () {
|
||||
// @TODO: ask about challenges when we add challenges
|
||||
await this.$store.dispatch('guilds:leave', {groupId: this.guild._id, type: 'myGuilds'});
|
||||
},
|
||||
async reject (invitationToReject) {
|
||||
// @TODO: This needs to be in the notifications where users will now accept invites
|
||||
let index = findIndex(this.user.invitations.guilds, function findInviteIndex (invite) {
|
||||
return invite.id === invitationToReject.id;
|
||||
});
|
||||
this.user.invitations.guilds = this.user.invitations.guilds.splice(0, index);
|
||||
await this.$store.dispatch('guilds:rejectInvite', {guildId: invitationToReject.id});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -20,10 +20,7 @@
|
||||
.col-6
|
||||
button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }}
|
||||
|
||||
.row.community-guidelines(v-if='!communityGuidelinesAccepted')
|
||||
div.col-8(v-once, v-html="$t('communityGuidelinesIntro')")
|
||||
div.col-4
|
||||
button.btn.btn-info(@click='acceptCommunityGuidelines()', v-once) {{ $t('acceptCommunityGuidelines') }}
|
||||
community-guidelines
|
||||
|
||||
.row
|
||||
.hr.col-12
|
||||
@@ -148,19 +145,6 @@
|
||||
.chat-row {
|
||||
position: relative;
|
||||
|
||||
.community-guidelines {
|
||||
background-color: rgba(135, 129, 144, 0.84);
|
||||
padding: 1em;
|
||||
color: $white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 150px;
|
||||
padding-top: 3em;
|
||||
margin-top: 2.3em;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
@@ -360,6 +344,7 @@ import { mapState } from 'client/libs/store';
|
||||
import { TAVERN_ID } from '../../../common/script/constants';
|
||||
import chatMessage from '../chat/chatMessages';
|
||||
import autocomplete from '../chat/autoComplete';
|
||||
import communityGuidelines from './communityGuidelines';
|
||||
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import questIcon from 'assets/svg/quest.svg';
|
||||
@@ -384,6 +369,7 @@ export default {
|
||||
components: {
|
||||
chatMessage,
|
||||
autocomplete,
|
||||
communityGuidelines,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -502,9 +488,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
communityGuidelinesAccepted () {
|
||||
return this.user.flags.communityGuidelinesAccepted;
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
this.group = await this.$store.dispatch('guilds:getGroup', {groupId: TAVERN_ID});
|
||||
@@ -542,9 +525,6 @@ export default {
|
||||
selectedAutocomplete (newText) {
|
||||
this.newMessage = newText;
|
||||
},
|
||||
acceptCommunityGuidelines () {
|
||||
this.$store.dispatch('user:set', {'flags.communityGuidelinesAccepted': true});
|
||||
},
|
||||
toggleSleep () {
|
||||
this.user.preferences.sleep = !this.user.preferences.sleep;
|
||||
this.$store.dispatch('user:sleep');
|
||||
|
||||
@@ -57,16 +57,16 @@ div
|
||||
a.dropdown-item(href="http://habitica.wikia.com/wiki/Habitica_Wiki", target='_blank') {{ $t('wiki') }}
|
||||
.user-menu.d-flex.align-items-center
|
||||
.item-with-icon(v-if="userHourglasses > 0")
|
||||
.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
|
||||
.top-menu-icon.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
|
||||
span {{ userHourglasses }}
|
||||
.item-with-icon
|
||||
.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
|
||||
.top-menu-icon.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
|
||||
span {{userGems | roundBigNumber}}
|
||||
.item-with-icon.gold
|
||||
.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
|
||||
.top-menu-icon.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
|
||||
span {{Math.floor(user.stats.gp * 100) / 100}}
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
.svg-icon(v-html="icons.sync")
|
||||
.top-menu-icon.svg-icon(v-html="icons.sync")
|
||||
notification-menu.item-with-icon
|
||||
user-dropdown.item-with-icon
|
||||
</template>
|
||||
@@ -144,6 +144,7 @@ div
|
||||
padding-right: 12.5px;
|
||||
height: 56px;
|
||||
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
|
||||
z-index: 1042; // To stay above snakbar notifications and modals
|
||||
}
|
||||
|
||||
.navbar-header {
|
||||
@@ -235,11 +236,11 @@ div
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&:hover /deep/ .svg-icon {
|
||||
&:hover /deep/ .top-menu-icon.svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
& /deep/ .svg-icon {
|
||||
& /deep/ .top-menu-icon.svg-icon {
|
||||
color: $header-color;
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template lang="pug" functional>
|
||||
span.message-count(:class="{'top-count': props.top === true}") {{props.count}}
|
||||
span.message-count(
|
||||
:class="{'top-count': props.top === true, 'top-count-gray': props.gray === true}"
|
||||
) {{props.count}}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -24,4 +26,8 @@ span.message-count(:class="{'top-count': props.top === true}") {{props.count}}
|
||||
padding: 0.2em;
|
||||
background-color: $red-50;
|
||||
}
|
||||
|
||||
.message-count.top-count-gray {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template lang="pug">
|
||||
.notification.dropdown-item.dropdown-separated.d-flex.justify-content-between(
|
||||
@click="clicked"
|
||||
)
|
||||
.notification-icon.d-flex.justify-content-center.align-items-center(
|
||||
v-if="hasIcon",
|
||||
:class="{'is-not-bailey': isNotBailey}",
|
||||
)
|
||||
slot(name="icon")
|
||||
.notification-content
|
||||
slot(name="content")
|
||||
.notification-remove(@click.stop="canRemove ? remove() : null",)
|
||||
.svg-icon(
|
||||
v-if="canRemove",
|
||||
v-html="icons.close",
|
||||
)
|
||||
</template>
|
||||
|
||||
<style lang="scss"> // Not scoped because the classes could be used in i18n strings
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
.notification-small {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.notification-ellipses {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notification-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.notification-bold-blue {
|
||||
font-weight: bold;
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
.notification-bold-purple {
|
||||
font-weight: bold;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.notification-yellow {
|
||||
color: #bf7d1a;
|
||||
}
|
||||
|
||||
.notification-green {
|
||||
color: #1CA372;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.notification {
|
||||
width: 378px;
|
||||
max-width: 100%;
|
||||
padding: 9px 20px 10px 24px;
|
||||
overflow: hidden;
|
||||
|
||||
&:active, &:hover {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
margin-right: 16px;
|
||||
|
||||
&.is-not-bailey {
|
||||
width: 31px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-buttons {
|
||||
margin-top: 12px;
|
||||
|
||||
.btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
// total distance from notification top and bottom edges are 15 and 16 pixels
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
flex-grow: 1;
|
||||
white-space: normal;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 1.43;
|
||||
color: $gray-50;
|
||||
|
||||
max-width: calc(100% - 26px); // to make space for the close icon
|
||||
}
|
||||
|
||||
.notification-remove {
|
||||
// total distance from the notification top edge is 20 pixels
|
||||
margin-top: 7px;
|
||||
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 12px;
|
||||
padding: 4px;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from 'assets/svg/close.svg';
|
||||
import { mapActions, mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove', 'hasIcon', 'readAfterClick'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
close: closeIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
isNotBailey () {
|
||||
return this.notification.type !== 'NEW_STUFF';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
readNotification: 'notifications:readNotification',
|
||||
}),
|
||||
clicked () {
|
||||
if (this.readAfterClick === true) {
|
||||
this.readNotification({notificationId: this.notification.id});
|
||||
}
|
||||
|
||||
this.$emit('click');
|
||||
},
|
||||
remove () {
|
||||
if (this.notification.type === 'NEW_CHAT_MESSAGE') {
|
||||
const groupId = this.notification.data.group.id;
|
||||
this.$store.dispatch('chat:markChatSeen', {groupId});
|
||||
if (this.user.newMessages[groupId]) {
|
||||
this.$delete(this.user.newMessages, groupId);
|
||||
}
|
||||
} else {
|
||||
this.readNotification({notificationId: this.notification.id});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="true",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="$t('cardReceived', {card: cardString})")
|
||||
div(slot="icon", :class="cardClass")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
cardString () {
|
||||
return this.$t(`${this.notification.data.card}Card`);
|
||||
},
|
||||
cardClass () {
|
||||
return `notif_inventory_special_${this.notification.data.card}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({name: 'items'});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template lang="pug" functional>
|
||||
div {{ props.notification }}
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
@click="action",
|
||||
)
|
||||
div(slot="content")
|
||||
div(v-html="notification.data.message")
|
||||
.notifications-buttons
|
||||
.btn.btn-small.btn-success(@click.stop="approve()") {{ $t('approve') }}
|
||||
.btn.btn-small.btn-warning(@click.stop="needsWork()") {{ $t('needsWork') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
// Check that the notification has all the necessary data (old ones are missing some fields)
|
||||
notificationHasData () {
|
||||
return Boolean(this.notification.data.groupTaskId && this.notification.data.userId);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
const groupId = this.notification.data.group.id;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
|
||||
},
|
||||
async approve () {
|
||||
// Redirect users to the group tasks page if the notification doesn't have data
|
||||
if (!this.notificationHasData) {
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: {
|
||||
groupId: this.notification.data.groupId,
|
||||
}});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(this.$t('confirmApproval'))) return;
|
||||
|
||||
this.$store.dispatch('tasks:approve', {
|
||||
taskId: this.notification.data.groupTaskId,
|
||||
userId: this.notification.data.userId,
|
||||
});
|
||||
},
|
||||
async needsWork () {
|
||||
// Redirect users to the group tasks page if the notification doesn't have data
|
||||
if (!this.notificationHasData) {
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: {
|
||||
groupId: this.notification.data.groupId,
|
||||
}});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(this.$t('confirmNeedsWork'))) return;
|
||||
|
||||
this.$store.dispatch('tasks:needsWork', {
|
||||
taskId: this.notification.data.groupTaskId,
|
||||
userId: this.notification.data.userId,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action",
|
||||
)
|
||||
.notification-green(slot="content", v-html="notification.data.message")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
const groupId = this.notification.data.groupId;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action",
|
||||
)
|
||||
.notification-yellow(slot="content", v-html="notification.data.message")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
const groupId = this.notification.data.group.id;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
@click="action",
|
||||
)
|
||||
div(slot="content")
|
||||
div(v-html="textString")
|
||||
.notifications-buttons
|
||||
.btn.btn-small.btn-success(@click.stop="accept()") {{ $t('accept') }}
|
||||
.btn.btn-small.btn-danger(@click.stop="reject()") {{ $t('reject') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
isPublicGuild () {
|
||||
if (this.notification.data.publicGuild === true) return true;
|
||||
return false;
|
||||
},
|
||||
textString () {
|
||||
const guild = this.notification.data.name;
|
||||
|
||||
if (this.isPublicGuild) {
|
||||
return this.$t('invitedToPublicGuild', {guild});
|
||||
} else {
|
||||
return this.$t('invitedToPrivateGuild', {guild});
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
if (!this.isPublicGuild) return;
|
||||
|
||||
const groupId = this.notification.data.id;
|
||||
|
||||
this.$router.push({ name: 'guild', params: { groupId } });
|
||||
},
|
||||
async accept () {
|
||||
const group = this.notification.data;
|
||||
|
||||
if (group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('guilds:join', {groupId: group.id, type: 'guild'});
|
||||
this.$router.push({ name: 'guild', params: { groupId: group.id } });
|
||||
},
|
||||
reject () {
|
||||
this.$store.dispatch('guilds:rejectInvite', {groupId: this.notification.data.id, type: 'guild'});
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
// Read automatically from the group page mounted hook
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
:read-after-click="false",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="string")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
groupId () {
|
||||
return this.notification.data.group.id;
|
||||
},
|
||||
isParty () {
|
||||
return this.groupId === this.user.party._id;
|
||||
},
|
||||
string () {
|
||||
const stringKey = this.isParty ? 'newMsgParty' : 'newMsgGuild';
|
||||
return this.$t(stringKey, {name: this.notification.data.group.name});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
if (this.isParty) {
|
||||
this.$router.push({ name: 'party' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push({ name: 'guild', params: { groupId: this.groupId }});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content")
|
||||
span(v-html="$t('userSentMessage', {user: notification.data.sender.name})")
|
||||
.notification-small.notification-ellipses {{ notification.data.excerpt }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'inbox-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="true",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="$t('newSubscriberItem')")
|
||||
div(slot="icon", :class="mysteryClass")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
mysteryClass () {
|
||||
return `notif_inventory_present_${moment().format('MM')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({name: 'items'});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="true",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content")
|
||||
.notification-bold-purple {{ $t('newBaileyUpdate') }}
|
||||
div {{ notification.data.title }}
|
||||
.npc_bailey(slot="icon")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
)
|
||||
div(slot="content")
|
||||
div(v-html="$t('invitedToParty', {party: notification.data.name})")
|
||||
.notifications-buttons
|
||||
.btn.btn-small.btn-success(@click.stop="accept()") {{ $t('accept') }}
|
||||
.btn.btn-small.btn-danger(@click.stop="reject()") {{ $t('reject') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
async accept () {
|
||||
const group = this.notification.data;
|
||||
|
||||
if (group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('guilds:join', {groupId: group.id, type: 'party'});
|
||||
this.$router.push('/party');
|
||||
},
|
||||
reject () {
|
||||
this.$store.dispatch('guilds:rejectInvite', {groupId: this.notification.data.id, type: 'party'});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
@click="action",
|
||||
)
|
||||
div(slot="content")
|
||||
.message(v-html="$t('invitedToQuest', {quest: questName})")
|
||||
quest-info(:quest="questData", :small-version="true")
|
||||
.notifications-buttons
|
||||
.btn.btn-small.btn-success(@click.stop="questAccept()") {{ $t('accept') }}
|
||||
.btn.btn-small.btn-danger(@click.stop="questReject()") {{ $t('reject') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.message {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import quests from 'common/script/content/quests';
|
||||
import questInfo from 'client/components/shops/quests/questInfo';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
questInfo,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
questData () {
|
||||
return quests.quests[this.notification.data.quest];
|
||||
},
|
||||
questName () {
|
||||
return this.questData.text();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({ name: 'party' });
|
||||
},
|
||||
async questAccept () {
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {
|
||||
groupId: this.notification.data.partyId,
|
||||
action: 'quests/accept',
|
||||
});
|
||||
this.user.party.quest = quest;
|
||||
},
|
||||
async questReject () {
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {
|
||||
groupId: this.notification.data.partyId,
|
||||
action: 'quests/reject',
|
||||
});
|
||||
this.user.party.quest = quest;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:has-icon="true",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="$t('unallocatedStatsPoints', {points: notification.data.points})")
|
||||
.svg-icon(slot="icon", v-html="icons.sparkles")
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.svg-icon {
|
||||
width: 23px;
|
||||
height: 28px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import sparklesIcon from 'assets/svg/sparkles.svg';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
sparkles: sparklesIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('habitica:show-profile', {
|
||||
user: this.$store.state.user.data,
|
||||
startingPage: 'stats',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,289 +1,254 @@
|
||||
<template lang="pug">
|
||||
menu-dropdown.item-notifications(:right="true")
|
||||
menu-dropdown.item-notifications(:right="true", @toggled="handleOpenStatusChange", :openStatus="openStatus")
|
||||
div(slot="dropdown-toggle")
|
||||
div(v-b-tooltip.hover.bottom="$t('notifications')")
|
||||
message-count(v-if='notificationsCount > 0', :count="notificationsCount", :top="true")
|
||||
.svg-icon.notifications(v-html="icons.notifications")
|
||||
message-count(
|
||||
v-if='notificationsCount > 0',
|
||||
:count="notificationsCount",
|
||||
:top="true",
|
||||
:gray="!hasUnseenNotifications",
|
||||
)
|
||||
.top-menu-icon.svg-icon.notifications(v-html="icons.notifications")
|
||||
div(slot="dropdown-content")
|
||||
h4.dropdown-item.dropdown-separated(v-if='!hasNoNotifications()') {{ $t('notifications') }}
|
||||
h4.dropdown-item.toolbar-notifs-no-messages(v-if='hasNoNotifications()') {{ $t('noNotifications') }}
|
||||
a.dropdown-item(v-if='user.party.quest && user.party.quest.RSVPNeeded')
|
||||
div {{ $t('invitedTo', {name: quests.quests[user.party.quest.key].text()}) }}
|
||||
div
|
||||
button.btn.btn-primary(@click.stop='questAccept(user.party._id)') Accept
|
||||
button.btn.btn-primary(@click.stop='questReject(user.party._id)') Reject
|
||||
a.dropdown-item(v-if='user.purchased.plan.mysteryItems.length', @click='go("/inventory/items")')
|
||||
span.glyphicon.glyphicon-gift
|
||||
span {{ $t('newSubscriberItem') }}
|
||||
a.dropdown-item(v-for='(party, index) in user.invitations.parties', :key='party.id')
|
||||
div
|
||||
span.glyphicon.glyphicon-user
|
||||
span {{ $t('invitedTo', {name: party.name}) }}
|
||||
div
|
||||
button.btn.btn-primary(@click.stop='accept(party, index, "party")') Accept
|
||||
button.btn.btn-primary(@click.stop='reject(party, index, "party")') Reject
|
||||
a.dropdown-item(v-if='user.flags.cardReceived', @click='go("/inventory/items")')
|
||||
span.glyphicon.glyphicon-envelope
|
||||
span {{ $t('cardReceived') }}
|
||||
a.dropdown-item(@click.stop='clearCards()')
|
||||
a.dropdown-item(v-for='(guild, index) in user.invitations.guilds', :key='guild.id')
|
||||
div
|
||||
span.glyphicon.glyphicon-user
|
||||
span {{ $t('invitedTo', {name: guild.name}) }}
|
||||
div
|
||||
button.btn.btn-primary(@click.stop='accept(guild, index, "guild")') Accept
|
||||
button.btn.btn-primary(@click.stop='reject(guild, index, "guild")') Reject
|
||||
a.dropdown-item(v-if='user.flags.classSelected && !user.preferences.disableClasses && user.stats.points',
|
||||
@click='showProfile()')
|
||||
span.glyphicon.glyphicon-plus-sign
|
||||
span {{ $t('haveUnallocated', {points: user.stats.points}) }}
|
||||
a.dropdown-item(v-for='message in userNewMessages', :key='message.key')
|
||||
span(@click='navigateToGroup(message.key)')
|
||||
span.glyphicon.glyphicon-comment
|
||||
span {{message.name}}
|
||||
span.clear-button(@click.stop='clearMessages(message.key)') Clear
|
||||
a.dropdown-item(v-for='notification in groupNotifications', :key='notification.id')
|
||||
span(:class="groupApprovalNotificationIcon(notification)")
|
||||
span {{notification.data.message}}
|
||||
span.clear-button(@click.stop='viewGroupApprovalNotification(notification)') Clear
|
||||
.dropdown-item.dropdown-separated.d-flex.justify-content-between.dropdown-inactive.align-items-center(
|
||||
@click.stop=""
|
||||
)
|
||||
h4.dropdown-title(v-once) {{ $t('notifications') }}
|
||||
a.small-link.standard-link(@click="dismissAll", :disabled="notificationsCount === 0") {{ $t('dismissAll') }}
|
||||
component(
|
||||
:is="notification.type",
|
||||
:key="notification.id",
|
||||
v-for="notification in notifications",
|
||||
:notification="notification",
|
||||
:can-remove="!isActionable(notification)",
|
||||
)
|
||||
.dropdown-item.dropdown-separated.d-flex.justify-content-center.dropdown-inactive.no-notifications.flex-column(
|
||||
v-if="notificationsCount === 0"
|
||||
)
|
||||
.svg-icon(v-html="icons.success")
|
||||
h2 You're all caught up!
|
||||
p The notification fairies give you a raucous round of applause! Well done!
|
||||
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.clear-button {
|
||||
margin-left: .5em;
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.dropdown-item {
|
||||
padding: 16px 24px;
|
||||
width: 378px;
|
||||
}
|
||||
|
||||
.dropdown-title {
|
||||
margin-bottom: 0px;
|
||||
margin-right: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.no-notifications {
|
||||
h2, p {
|
||||
text-align: center;
|
||||
color: $gray-200 !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
white-space: normal;
|
||||
margin-bottom: 43px;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin: 0 auto;
|
||||
width: 256px;
|
||||
height: 104px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import map from 'lodash/map';
|
||||
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import { mapState, mapActions } from 'client/libs/store';
|
||||
import quests from 'common/script/content/quests';
|
||||
import notificationsIcon from 'assets/svg/notifications.svg';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
import MessageCount from './messageCount';
|
||||
import successImage from 'assets/svg/success.svg';
|
||||
|
||||
// Notifications
|
||||
import NEW_STUFF from './notifications/newStuff';
|
||||
import GROUP_TASK_NEEDS_WORK from './notifications/groupTaskNeedsWork';
|
||||
import GUILD_INVITATION from './notifications/guildInvitation';
|
||||
import PARTY_INVITATION from './notifications/partyInvitation';
|
||||
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 UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
|
||||
import NEW_MYSTERY_ITEMS from './notifications/newMysteryItems';
|
||||
import CARD_RECEIVED from './notifications/cardReceived';
|
||||
import NEW_INBOX_MESSAGE from './notifications/newInboxMessage';
|
||||
import NEW_CHAT_MESSAGE from './notifications/newChatMessage';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuDropdown,
|
||||
MessageCount,
|
||||
},
|
||||
directives: {
|
||||
// bTooltip,
|
||||
// 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,
|
||||
UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED,
|
||||
NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
notifications: notificationsIcon,
|
||||
success: successImage,
|
||||
}),
|
||||
quests,
|
||||
openStatus: undefined,
|
||||
actionableNotifications: [
|
||||
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
|
||||
'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK',
|
||||
],
|
||||
// A list of notifications handled by this component,
|
||||
// listed in the order they should appear in the notifications panel.
|
||||
// NOTE: Those not listed here won't be shown in the notification panel!
|
||||
handledNotifications: [
|
||||
'NEW_STUFF', 'GROUP_TASK_NEEDS_WORK',
|
||||
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
|
||||
'QUEST_INVITATION', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
|
||||
'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
|
||||
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
party () {
|
||||
return {name: ''};
|
||||
// return this.user.party;
|
||||
notificationsOrder () {
|
||||
// Returns a map of NOTIFICATION_TYPE -> POSITION
|
||||
const orderMap = {};
|
||||
|
||||
this.handledNotifications.forEach((type, index) => {
|
||||
orderMap[type] = index;
|
||||
});
|
||||
|
||||
return orderMap;
|
||||
},
|
||||
userNewMessages () {
|
||||
// @TODO: For some reason data becomes corrupted. We should fix this on the server
|
||||
let userNewMessages = [];
|
||||
for (let key in this.user.newMessages) {
|
||||
let message = this.user.newMessages[key];
|
||||
if (message && message.name && message.value) {
|
||||
message.key = key;
|
||||
userNewMessages.push(message);
|
||||
notifications () {
|
||||
// Convert the notifications not stored in user.notifications
|
||||
const notifications = [];
|
||||
|
||||
// Parties invitations
|
||||
notifications.push(...this.user.invitations.parties.map(partyInvitation => {
|
||||
return {
|
||||
type: 'PARTY_INVITATION',
|
||||
data: partyInvitation,
|
||||
// Create a custom id for notifications outside user.notifications (must be unique)
|
||||
id: `custom-party-invitation-${partyInvitation.id}`,
|
||||
};
|
||||
}));
|
||||
|
||||
// Guilds invitations
|
||||
notifications.push(...this.user.invitations.guilds.map(guildInvitation => {
|
||||
return {
|
||||
type: 'GUILD_INVITATION',
|
||||
data: guildInvitation,
|
||||
// Create a custom id for notifications outside user.notifications (must be unique)
|
||||
id: `custom-guild-invitation-${guildInvitation.id}`,
|
||||
};
|
||||
}));
|
||||
|
||||
// Quest invitation
|
||||
if (this.user.party.quest.RSVPNeeded === true) {
|
||||
notifications.push({
|
||||
type: 'QUEST_INVITATION',
|
||||
data: {
|
||||
quest: this.user.party.quest.key,
|
||||
partyId: this.user.party._id,
|
||||
},
|
||||
// Create a custom id for notifications outside user.notifications (must be unique)
|
||||
id: `custom-quest-invitation-${this.user.party._id}`,
|
||||
});
|
||||
}
|
||||
|
||||
const orderMap = this.notificationsOrder;
|
||||
|
||||
// Push the notifications stored in user.notifications
|
||||
// skipping those not defined in the handledNotifications object
|
||||
notifications.push(...this.user.notifications.filter(notification => {
|
||||
if (notification.type === 'UNALLOCATED_STATS_POINTS') {
|
||||
if (!this.user.flags.classSelected || this.user.preferences.disableClasses) return false;
|
||||
}
|
||||
}
|
||||
return userNewMessages;
|
||||
},
|
||||
groupNotifications () {
|
||||
return this.$store.state.groupNotifications;
|
||||
|
||||
return orderMap[notification.type] !== undefined;
|
||||
}));
|
||||
|
||||
// Sort notifications
|
||||
notifications.sort((a, b) => { // a and b are notifications
|
||||
const aOrder = orderMap[a.type];
|
||||
const bOrder = orderMap[b.type];
|
||||
|
||||
if (aOrder === bOrder) return 0; // Same position
|
||||
if (aOrder > bOrder) return 1; // b is higher
|
||||
if (aOrder < bOrder) return -1; // a is higher
|
||||
});
|
||||
|
||||
return notifications;
|
||||
},
|
||||
// The total number of notification, shown inside the dropdown
|
||||
notificationsCount () {
|
||||
let count = 0;
|
||||
|
||||
if (this.user.invitations.parties) {
|
||||
count += this.user.invitations.parties.length;
|
||||
}
|
||||
|
||||
if (this.user.purchased.plan && this.user.purchased.plan.mysteryItems.length) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (this.user.invitations.guilds) {
|
||||
count += this.user.invitations.guilds.length;
|
||||
}
|
||||
|
||||
if (this.user.flags.classSelected && !this.user.preferences.disableClasses && this.user.stats.points) {
|
||||
count += this.user.stats.points > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
if (this.userNewMessages) {
|
||||
count += Object.keys(this.userNewMessages).length;
|
||||
}
|
||||
|
||||
count += this.groupNotifications.length;
|
||||
|
||||
return count;
|
||||
return this.notifications.length;
|
||||
},
|
||||
hasUnseenNotifications () {
|
||||
return this.notifications.some((notification) => {
|
||||
return notification.seen === false ? true : false;
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// @TODO: I hate this function, we can do better with a hashmap
|
||||
selectNotificationValue (mysteryValue, invitationValue, cardValue,
|
||||
unallocatedValue, messageValue, noneValue, groupApprovalRequested, groupApproved) {
|
||||
let user = this.user;
|
||||
...mapActions({
|
||||
readNotifications: 'notifications:readNotifications',
|
||||
seeNotifications: 'notifications:seeNotifications',
|
||||
}),
|
||||
handleOpenStatusChange (openStatus) {
|
||||
this.openStatus = openStatus === true ? 1 : 0;
|
||||
|
||||
if (user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems && user.purchased.plan.mysteryItems.length) {
|
||||
return mysteryValue;
|
||||
} else if (user.invitations.parties && user.invitations.parties.length > 0 || user.invitations.guilds && user.invitations.guilds.length > 0) {
|
||||
return invitationValue;
|
||||
} else if (user.flags.cardReceived) {
|
||||
return cardValue;
|
||||
} else if (user.flags.classSelected && !(user.preferences && user.preferences.disableClasses) && user.stats.points) {
|
||||
return unallocatedValue;
|
||||
} else if (!isEmpty(user.newMessages)) {
|
||||
return messageValue;
|
||||
} else if (!isEmpty(this.groupNotifications)) {
|
||||
let groupNotificationTypes = map(this.groupNotifications, 'type');
|
||||
if (groupNotificationTypes.indexOf('GROUP_TASK_APPROVAL') !== -1) {
|
||||
return groupApprovalRequested;
|
||||
} else if (groupNotificationTypes.indexOf('GROUP_TASK_APPROVED') !== -1) {
|
||||
return groupApproved;
|
||||
// Mark notifications as seen when the menu is opened
|
||||
if (openStatus) this.markAllAsSeen();
|
||||
},
|
||||
markAllAsSeen () {
|
||||
const idsToSee = this.notifications.map(notification => {
|
||||
// We check explicitly for notification.id not starting with `custom-` because some
|
||||
// notification don't follow the standard
|
||||
// (all those not stored in user.notifications)
|
||||
if (notification.seen === false && notification.id && notification.id.indexOf('custom-') !== 0) {
|
||||
return notification.id;
|
||||
}
|
||||
return noneValue;
|
||||
} else {
|
||||
return noneValue;
|
||||
}
|
||||
},
|
||||
hasQuestProgress () {
|
||||
let user = this.user;
|
||||
if (user.party.quest) {
|
||||
let userQuest = quests[user.party.quest.key];
|
||||
}).filter(id => Boolean(id));
|
||||
|
||||
if (!userQuest) {
|
||||
return false;
|
||||
if (idsToSee.length > 0) this.seeNotifications({notificationIds: idsToSee});
|
||||
},
|
||||
dismissAll () {
|
||||
const idsToRead = this.notifications.map(notification => {
|
||||
// We check explicitly for notification.id not starting with `custom-` because some
|
||||
// notification don't follow the standard
|
||||
// (all those not stored in user.notifications)
|
||||
if (!this.isActionable(notification) && notification.id.indexOf('custom-') !== 0) {
|
||||
return notification.id;
|
||||
}
|
||||
if (userQuest.boss && user.party.quest.progress.up > 0) {
|
||||
return true;
|
||||
}
|
||||
if (userQuest.collect && user.party.quest.progress.collectedItems > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getQuestInfo () {
|
||||
let user = this.user;
|
||||
let questInfo = {};
|
||||
if (user.party.quest) {
|
||||
let userQuest = quests[user.party.quest.key];
|
||||
}).filter(id => Boolean(id));
|
||||
this.openStatus = 0;
|
||||
|
||||
questInfo.title = userQuest.text();
|
||||
|
||||
if (userQuest.boss) {
|
||||
questInfo.body = this.$t('questTaskDamage', { damage: user.party.quest.progress.up.toFixed(1) });
|
||||
} else if (userQuest.collect) {
|
||||
questInfo.body = this.$t('questTaskCollection', { items: user.party.quest.progress.collectedItems });
|
||||
}
|
||||
}
|
||||
return questInfo;
|
||||
if (idsToRead.length > 0) this.readNotifications({notificationIds: idsToRead});
|
||||
},
|
||||
clearMessages (key) {
|
||||
this.$store.dispatch('chat:markChatSeen', {groupId: key});
|
||||
this.$delete(this.user.newMessages, key);
|
||||
},
|
||||
clearCards () {
|
||||
this.$store.dispatch('chat:clearCards');
|
||||
},
|
||||
iconClasses () {
|
||||
return this.selectNotificationValue(
|
||||
'glyphicon-gift',
|
||||
'glyphicon-user',
|
||||
'glyphicon-envelope',
|
||||
'glyphicon-plus-sign',
|
||||
'glyphicon-comment',
|
||||
'glyphicon-comment inactive',
|
||||
'glyphicon-question-sign',
|
||||
'glyphicon-ok-sign'
|
||||
);
|
||||
},
|
||||
hasNoNotifications () {
|
||||
return this.selectNotificationValue(false, false, false, false, false, true, false, false);
|
||||
},
|
||||
viewGroupApprovalNotification (notification) {
|
||||
this.$store.state.groupNotifications = this.groupNotifications.filter(groupNotif => {
|
||||
return groupNotif.id !== notification.id;
|
||||
});
|
||||
|
||||
axios.post('/api/v3/notifications/read', {
|
||||
notificationIds: [notification.id],
|
||||
});
|
||||
},
|
||||
groupApprovalNotificationIcon (notification) {
|
||||
if (notification.type === 'GROUP_TASK_APPROVAL') {
|
||||
return 'glyphicon glyphicon-question-sign';
|
||||
} else if (notification.type === 'GROUP_TASK_APPROVED') {
|
||||
return 'glyphicon glyphicon-ok-sign';
|
||||
}
|
||||
},
|
||||
go (path) {
|
||||
this.$router.push(path);
|
||||
},
|
||||
navigateToGroup (key) {
|
||||
if (key === this.party._id || key === this.user.party._id) {
|
||||
this.go('/party');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push({ name: 'guild', params: { groupId: key }});
|
||||
},
|
||||
async reject (group) {
|
||||
await this.$store.dispatch('guilds:rejectInvite', {groupId: group.id});
|
||||
// @TODO: User.sync();
|
||||
},
|
||||
async accept (group, index, type) {
|
||||
if (group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'party') {
|
||||
// @TODO: pretty sure mutability is wrong. Need to check React docs
|
||||
// @TODO mutation to store data should only happen through actions
|
||||
this.user.invitations.parties.splice(index, 1);
|
||||
|
||||
Analytics.updateUser({partyID: group.id});
|
||||
} else {
|
||||
this.user.invitations.guilds.splice(index, 1);
|
||||
}
|
||||
|
||||
if (type === 'party') {
|
||||
this.user.party._id = group.id;
|
||||
this.$router.push('/party');
|
||||
} else {
|
||||
this.user.guilds.push(group.id);
|
||||
this.$router.push(`/groups/guild/${group.id}`);
|
||||
}
|
||||
|
||||
// @TODO: check for party , type: 'myGuilds'
|
||||
await this.$store.dispatch('guilds:join', {guildId: group.id});
|
||||
},
|
||||
async questAccept (partyId) {
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {groupId: partyId, action: 'quests/accept'});
|
||||
this.user.party.quest = quest;
|
||||
},
|
||||
async questReject (partyId) {
|
||||
let quest = await this.$store.dispatch('quests:sendAction', {groupId: partyId, action: 'quests/reject'});
|
||||
this.user.party.quest = quest;
|
||||
},
|
||||
showProfile () {
|
||||
this.$root.$emit('habitica:show-profile', {
|
||||
user: this.user,
|
||||
startingPage: 'stats',
|
||||
});
|
||||
isActionable (notification) {
|
||||
return this.actionableNotifications.indexOf(notification.type) !== -1;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ menu-dropdown.item-user(:right="true")
|
||||
div(slot="dropdown-toggle")
|
||||
div(v-b-tooltip.hover.bottom="$t('user')")
|
||||
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages", :top="true")
|
||||
.svg-icon.user(v-html="icons.user")
|
||||
.top-menu-icon.svg-icon.user(v-html="icons.user")
|
||||
.user-dropdown(slot="dropdown-content")
|
||||
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
|
||||
h3 {{ user.profile.name }}
|
||||
|
||||
@@ -156,6 +156,19 @@ export default {
|
||||
let lastShownNotifications = [];
|
||||
let alreadyReadNotification = [];
|
||||
|
||||
// A list of notifications handled by this component,
|
||||
// NOTE: Those not listed here won't be handled at all!
|
||||
const handledNotifications = {};
|
||||
|
||||
[
|
||||
'GUILD_PROMPT', 'DROPS_ENABLED', 'REBIRTH_ENABLED', 'WON_CHALLENGE', 'STREAK_ACHIEVEMENT',
|
||||
'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT',
|
||||
'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL',
|
||||
'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE',
|
||||
].forEach(type => {
|
||||
handledNotifications[type] = true;
|
||||
});
|
||||
|
||||
return {
|
||||
yesterDailies: [],
|
||||
levelBeforeYesterdailies: 0,
|
||||
@@ -165,54 +178,31 @@ export default {
|
||||
alreadyReadNotification,
|
||||
isRunningYesterdailies: false,
|
||||
nextCron: null,
|
||||
handledNotifications,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
// https://stackoverflow.com/questions/42133894/vue-js-how-to-properly-watch-for-nested-properties/42134176#42134176
|
||||
baileyShouldShow () {
|
||||
return this.user.flags.newStuff;
|
||||
},
|
||||
userHp () {
|
||||
return this.user.stats.hp;
|
||||
},
|
||||
userExp () {
|
||||
return this.user.stats.exp;
|
||||
},
|
||||
userGp () {
|
||||
return this.user.stats.gp;
|
||||
},
|
||||
userMp () {
|
||||
return this.user.stats.mp;
|
||||
},
|
||||
userLvl () {
|
||||
return this.user.stats.lvl;
|
||||
},
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
userHp: 'user.data.stats.hp',
|
||||
userExp: 'user.data.stats.exp',
|
||||
userGp: 'user.data.stats.gp',
|
||||
userMp: 'user.data.stats.mp',
|
||||
userLvl: 'user.data.stats.lvl',
|
||||
userNotifications: 'user.data.notifications',
|
||||
userAchievements: 'user.data.achievements', // @TODO: does this watch deeply?
|
||||
armoireEmpty: 'user.data.flags.armoireEmpty',
|
||||
questCompleted: 'user.data.party.quest.completed',
|
||||
}),
|
||||
userClassSelect () {
|
||||
return !this.user.flags.classSelected && this.user.stats.lvl >= 10;
|
||||
},
|
||||
userNotifications () {
|
||||
return this.user.notifications;
|
||||
},
|
||||
userAchievements () {
|
||||
// @TODO: does this watch deeply?
|
||||
return this.user.achievements;
|
||||
},
|
||||
armoireEmpty () {
|
||||
return this.user.flags.armoireEmpty;
|
||||
},
|
||||
questCompleted () {
|
||||
return this.user.party.quest.completed;
|
||||
},
|
||||
invitedToQuest () {
|
||||
return this.user.party.quest.RSVPNeeded && !this.user.party.quest.completed;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
baileyShouldShow () {
|
||||
if (this.user.needsCron) return;
|
||||
this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||
},
|
||||
userHp (after, before) {
|
||||
if (after <= 0) {
|
||||
this.playSound('Death');
|
||||
@@ -425,9 +415,6 @@ export default {
|
||||
this.scheduleNextCron();
|
||||
this.handleUserNotifications(this.user.notifications);
|
||||
},
|
||||
transferGroupNotification (notification) {
|
||||
this.$store.state.groupNotifications.push(notification);
|
||||
},
|
||||
async handleUserNotifications (after) {
|
||||
if (this.$store.state.isRunningYesterdailies) return;
|
||||
|
||||
@@ -440,23 +427,21 @@ export default {
|
||||
let notificationsToRead = [];
|
||||
let scoreTaskNotification = [];
|
||||
|
||||
this.$store.state.groupNotifications = []; // Flush group notifictions
|
||||
|
||||
after.forEach((notification) => {
|
||||
// This notification type isn't implemented here
|
||||
if (!this.handledNotifications[notification.type]) return;
|
||||
|
||||
if (this.lastShownNotifications.indexOf(notification.id) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Some notifications are not marked read here, so we need to fix this system
|
||||
// to handle notifications differently
|
||||
if (['GROUP_TASK_APPROVED', 'GROUP_TASK_APPROVAL'].indexOf(notification.type) === -1) {
|
||||
this.lastShownNotifications.push(notification.id);
|
||||
if (this.lastShownNotifications.length > 10) {
|
||||
this.lastShownNotifications.splice(0, 9);
|
||||
}
|
||||
this.lastShownNotifications.push(notification.id);
|
||||
if (this.lastShownNotifications.length > 10) {
|
||||
this.lastShownNotifications.splice(0, 9);
|
||||
}
|
||||
|
||||
let markAsRead = true;
|
||||
|
||||
// @TODO: Use factory function instead
|
||||
switch (notification.type) {
|
||||
case 'GUILD_PROMPT':
|
||||
@@ -513,14 +498,6 @@ export default {
|
||||
if (notification.data.mp) this.mp(notification.data.mp);
|
||||
}
|
||||
break;
|
||||
case 'GROUP_TASK_APPROVAL':
|
||||
this.transferGroupNotification(notification);
|
||||
markAsRead = false;
|
||||
break;
|
||||
case 'GROUP_TASK_APPROVED':
|
||||
this.transferGroupNotification(notification);
|
||||
markAsRead = false;
|
||||
break;
|
||||
case 'SCORED_TASK':
|
||||
// Search if it is a read notification
|
||||
for (let i = 0; i < this.alreadyReadNotification.length; i++) {
|
||||
@@ -544,16 +521,6 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'login-incentives');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (notification.data.headerText && notification.data.bodyText) {
|
||||
// @TODO:
|
||||
// let modalScope = this.$new();
|
||||
// modalScope.data = notification.data;
|
||||
// this.openModal('generic', {scope: modalScope});
|
||||
} else {
|
||||
markAsRead = false; // If the notification is not implemented, skip it
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (markAsRead) notificationsToRead.push(notification.id);
|
||||
@@ -567,6 +534,7 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO this code is never run because userReadNotifsPromise is never true
|
||||
if (userReadNotifsPromise) {
|
||||
userReadNotifsPromise.then(() => {
|
||||
// Only run this code for scoring approved tasks
|
||||
@@ -595,8 +563,6 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
this.user.notifications = []; // reset the notifications
|
||||
|
||||
this.checkUserAchievements();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ b-modal#send-gems(:title="title", :hide-footer="true", size='lg')
|
||||
button.btn.btn-primary(@click='showStripe({gift, uuid: userReceivingGems._id})') {{ $t('card') }}
|
||||
button.btn.btn-warning(@click='openPaypalGift({gift: gift, giftedTo: userReceivingGems._id})') PayPal
|
||||
button.btn.btn-success(@click="amazonPaymentsInit({type: 'single', gift, giftedTo: userReceivingGems._id})") Amazon Payments
|
||||
button.btn.btn-secondary(@click='close()') {{$t('cancel')}}
|
||||
button.btn.btn-secondary(@click='close()') {{$t('cancel')}}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
h5 {{ $t('characterBuild') }}
|
||||
h6(v-once) {{ $t('class') + ': ' }}
|
||||
// @TODO: what is classText
|
||||
span(v-if='classText') {{ classText }}
|
||||
// span(v-if='classText') {{ classText }}
|
||||
button.btn.btn-danger.btn-xs(@click='changeClassForUser(true)', v-once) {{ $t('changeClass') }}
|
||||
small.cost 3 {{ $t('gems') }}
|
||||
// @TODO add icon span.Pet_Currency_Gem1x.inline-gems
|
||||
@@ -291,7 +291,6 @@ export default {
|
||||
// Guide.goto('intro', 0, true);
|
||||
},
|
||||
showBailey () {
|
||||
this.user.flags.newStuff = true;
|
||||
this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||
},
|
||||
hasBackupAuthOption (networkKeyToCheck) {
|
||||
|
||||
@@ -1,40 +1,52 @@
|
||||
<template lang="pug">
|
||||
div.row
|
||||
span.col-4(v-if="quest.collect") {{ $t('collect') + ':' }}
|
||||
span.col-8(v-if="quest.collect")
|
||||
.row(:class="{'small-version': smallVersion}")
|
||||
template(v-if="quest.collect")
|
||||
span.title(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('collect') + ':' }}
|
||||
span.col-8
|
||||
div(v-for="(collect, key) of quest.collect")
|
||||
span {{ collect.count }} {{ getCollectText(collect) }}
|
||||
|
||||
span.col-4(v-if="quest.boss") {{ $t('bossHP') + ':' }}
|
||||
span.col-8(v-if="quest.boss") {{ quest.boss.hp }}
|
||||
template(v-if="quest.boss")
|
||||
span.title(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('bossHP') + ':' }}
|
||||
span.col-8 {{ quest.boss.hp }}
|
||||
|
||||
span.col-4 {{ $t('difficulty') + ':' }}
|
||||
span.col-8
|
||||
span.svg-icon.inline.icon-16(v-for="star of stars()", v-html="icons[star]")
|
||||
span.title(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('difficulty') + ':' }}
|
||||
span.col-8
|
||||
.svg-icon.inline(
|
||||
v-for="star of stars()", v-html="icons[star]",
|
||||
:class="smallVersion ? 'icon-12' : 'icon-16'",
|
||||
)
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
.title {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-4{
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
height: 16px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.col-8 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.col-8:not(:last-child) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
span.svg-icon.inline.icon-16 {
|
||||
margin-right: 4px;
|
||||
.col-8 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.col-8:not(:last-child) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.small-version {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
|
||||
.svg-icon {
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -43,6 +55,15 @@
|
||||
import svgStarEmpty from 'assets/svg/difficulty-star-empty.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
quest: {
|
||||
type: Object,
|
||||
},
|
||||
smallVersion: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
@@ -88,10 +109,5 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
props: {
|
||||
quest: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template lang="pug">
|
||||
#front
|
||||
#front.static-view
|
||||
noscript.banner {{ $t('jsDisabledHeadingFull') }}
|
||||
br
|
||||
a(href='http://www.enable-javascript.com/', target='_blank') {{ $t('jsDisabledLink') }}
|
||||
@@ -118,8 +118,12 @@
|
||||
.seamless_stars_varied_opacity_repeat
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~client/assets/scss/static.scss';
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/static.scss';
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
#front {
|
||||
.form-text a {
|
||||
|
||||
@@ -1,60 +1,23 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
.media
|
||||
.promo_starry_potions.right-margin
|
||||
.media-body
|
||||
.media
|
||||
.align-self-center.right-margin(:class='baileyClass')
|
||||
.media-body
|
||||
h1.align-self-center(v-markdown='$t("newStuff")')
|
||||
h2 1/31/2018 - LAST CHANCE FOR WINTER WONDERLAND OUTFITS, WINTER CUSTOMIZATIONS, AND SNOWBALLS
|
||||
hr
|
||||
p(v-markdown='"Today is the final day of the Winter Wonderland Festival, so if you still have any remaining Winter Wonderland Items that you want to buy, you\'d better do it now! The [Seasonal Edition items](/shops/seasonal) and avatar customizations won\'t be back until next December, and if the Limited Edition items return they will have increased prices or changed art, so be sure to snag them today!"')
|
||||
.promo_winter_customizations.left-margin
|
||||
hr
|
||||
.static-view(v-html="html")
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/static.scss';
|
||||
.center-block {
|
||||
margin: 0 auto 1em auto;
|
||||
}
|
||||
|
||||
.left-margin {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.right-margin {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.bottom-margin {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.small {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
<style lang='scss'>
|
||||
@import '~client/assets/scss/static.scss';
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import markdown from 'client/directives/markdown';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
let worldDmg = {
|
||||
bailey: false,
|
||||
};
|
||||
|
||||
return {
|
||||
baileyClass: {
|
||||
'npc_bailey_broken': worldDmg.bailey, // eslint-disable-line
|
||||
'npc_bailey': !worldDmg.bailey, // eslint-disable-line
|
||||
},
|
||||
};
|
||||
},
|
||||
directives: {
|
||||
markdown,
|
||||
},
|
||||
};
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
html: '',
|
||||
};
|
||||
},
|
||||
async mounted () {
|
||||
let response = await axios.get('/api/v3/news');
|
||||
this.html = response.data.html;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template lang="pug">
|
||||
.container-fluid
|
||||
.container-fluid.static-view
|
||||
.row
|
||||
.col-md-6.offset-3
|
||||
h1 {{ $t('overview') }}
|
||||
@@ -11,9 +11,11 @@
|
||||
p(v-markdown="$t('overviewQuestions')")
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
<style lang='scss'>
|
||||
@import '~client/assets/scss/static.scss';
|
||||
</style>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.container-fluid {
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@ div
|
||||
#purple-footer {
|
||||
background-color: #271b3d;
|
||||
|
||||
.row {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
footer, footer a {
|
||||
background: transparent;
|
||||
color: #d5c8ff;
|
||||
@@ -113,6 +117,7 @@ div
|
||||
.static-wrapper {
|
||||
.container-fluid {
|
||||
margin: 5em 2em 2em 2em;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
|
||||
@@ -4,15 +4,15 @@ div
|
||||
.claim-bottom-message.col-12
|
||||
.task-unclaimed.d-flex.justify-content-between(v-if='!approvalRequested && !multipleApprovalsRequested')
|
||||
span {{ message }}
|
||||
a.text-right(@click='claim()', v-if='!userIsAssigned') Claim
|
||||
a.text-right(@click='unassign()', v-if='userIsAssigned') Remove Claim
|
||||
a.text-right(@click='claim()', v-if='!userIsAssigned') {{ $t('claim') }}
|
||||
a.text-right(@click='unassign()', v-if='userIsAssigned') {{ $t('removeClaim') }}
|
||||
.row.task-single-approval(v-if='approvalRequested')
|
||||
.col-6.text-center
|
||||
a(@click='approve()') Approve Task
|
||||
// @TODO: Implement in v2 .col-6.text-center
|
||||
a Needs work
|
||||
a(@click='approve()') {{ $t('approveTask') }}
|
||||
.col-6.text-center
|
||||
a(@click='needsWork()') {{ $t('needsWork') }}
|
||||
.text-center.task-multi-approval(v-if='multipleApprovalsRequested')
|
||||
a(@click='showRequests()') View Requests
|
||||
a(@click='showRequests()') {{ $t('viewRequests') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss", scoped>
|
||||
@@ -116,15 +116,21 @@ export default {
|
||||
approve () {
|
||||
if (!confirm(this.$t('confirmApproval'))) return;
|
||||
let userIdToApprove = this.task.group.assignedUsers[0];
|
||||
this.$store.dispatch('tasks:unassignTask', {
|
||||
this.$store.dispatch('tasks:approve', {
|
||||
taskId: this.task._id,
|
||||
userId: userIdToApprove,
|
||||
});
|
||||
this.task.group.assignedUsers.splice(0, 1);
|
||||
this.task.approvals.splice(0, 1);
|
||||
},
|
||||
reject () {
|
||||
|
||||
needsWork () {
|
||||
if (!confirm(this.$t('confirmNeedsWork'))) return;
|
||||
let userIdNeedsMoreWork = this.task.group.assignedUsers[0];
|
||||
this.$store.dispatch('tasks:needsWork', {
|
||||
taskId: this.task._id,
|
||||
userId: userIdNeedsMoreWork,
|
||||
});
|
||||
this.task.approvals.splice(0, 1);
|
||||
},
|
||||
showRequests () {
|
||||
this.$root.$emit('bv::show::modal', 'approval-modal');
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template lang="pug">
|
||||
b-modal#approval-modal(title="Approve Task", size='md', :hide-footer="true")
|
||||
b-modal#approval-modal(:title="$t('approveTask')", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.row.approval(v-for='(approval, index) in task.approvals')
|
||||
.col-8
|
||||
strong {{approval.userId.profile.name}}
|
||||
.col-2
|
||||
button.btn.btn-primary(@click='approve(index)') Approve
|
||||
button.btn.btn-primary(@click='approve(index)') {{ $t('approve') }}
|
||||
.col-2
|
||||
button.btn.btn-secondary(@click='needsWork(index)') {{ $t('needsWork') }}
|
||||
.modal-footer
|
||||
button.btn.btn-secondary(@click='close()') {{$t('close')}}
|
||||
</template>
|
||||
@@ -22,15 +24,24 @@ export default {
|
||||
props: ['task'],
|
||||
methods: {
|
||||
approve (index) {
|
||||
if (!confirm('Are you sure you want to approve this task?')) return;
|
||||
if (!confirm(this.$t('confirmApproval'))) return;
|
||||
let userIdToApprove = this.task.group.assignedUsers[index];
|
||||
this.$store.dispatch('tasks:unassignTask', {
|
||||
this.$store.dispatch('tasks:approve', {
|
||||
taskId: this.task._id,
|
||||
userId: userIdToApprove,
|
||||
});
|
||||
this.task.group.assignedUsers.splice(index, 1);
|
||||
this.task.approvals.splice(index, 1);
|
||||
},
|
||||
needsWork (index) {
|
||||
if (!confirm(this.$t('confirmNeedsWork'))) return;
|
||||
let userIdNeedsMoreWork = this.task.group.assignedUsers[index];
|
||||
this.$store.dispatch('tasks:needsWork', {
|
||||
taskId: this.task._id,
|
||||
userId: userIdNeedsMoreWork,
|
||||
});
|
||||
this.task.approvals.splice(index, 1);
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'approval-modal');
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ A simplified dropdown component that doesn't rely on buttons as toggles like bo
|
||||
-->
|
||||
|
||||
<template lang="pug">
|
||||
.habitica-menu-dropdown.item-with-icon.dropdown(@click="toggleDropdown()", :class="{open: isDropdownOpen}")
|
||||
.habitica-menu-dropdown.dropdown(@click="toggleDropdown()", :class="{open: isOpen}")
|
||||
.habitica-menu-dropdown-toggle
|
||||
slot(name="dropdown-toggle")
|
||||
.dropdown-menu(:class="{'dropdown-menu-right': right}")
|
||||
@@ -43,7 +43,7 @@ A simplified dropdown component that doesn't rely on buttons as toggles like bo
|
||||
&.open {
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,12 +51,22 @@ A simplified dropdown component that doesn't rely on buttons as toggles like bo
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['right'],
|
||||
props: {
|
||||
right: Boolean,
|
||||
openStatus: Number,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isDropdownOpen: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isOpen () {
|
||||
// Open status is a number so we can tell if the value was passed
|
||||
if (this.openStatus !== undefined) return this.openStatus === 1 ? true : false;
|
||||
return this.isDropdownOpen;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
document.documentElement.addEventListener('click', this._clickOutListener);
|
||||
},
|
||||
@@ -65,12 +75,13 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
_clickOutListener (e) {
|
||||
if (!this.$el.contains(e.target) && this.isDropdownOpen) {
|
||||
if (!this.$el.contains(e.target) && this.isOpen) {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
},
|
||||
toggleDropdown () {
|
||||
this.isDropdownOpen = !this.isDropdownOpen;
|
||||
this.isDropdownOpen = !this.isOpen;
|
||||
this.$emit('toggled', this.isDropdownOpen);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -255,7 +255,7 @@ div
|
||||
li
|
||||
strong {{$t('buffs')}}:
|
||||
| {{user.stats.buffs[stat]}}
|
||||
#allocation(v-if='user._id === userLoggedIn._id')
|
||||
#allocation(v-if='user._id === userLoggedIn._id && user.flags.classSelected && !user.preferences.disableClasses')
|
||||
.row.title-row
|
||||
.col-12.col-md-6
|
||||
h3(v-if='userLevel100Plus', v-once, v-html="$t('noMoreAllocate')")
|
||||
|
||||
@@ -10,6 +10,13 @@ export async function createChallenge (store, payload) {
|
||||
return newChallenge;
|
||||
}
|
||||
|
||||
export async function cloneChallenge (store, payload) {
|
||||
const response = await axios.post(`/api/v3/challenges/${payload.cloningChallengeId}/clone`, payload.challenge);
|
||||
const newChallenge = response.data.data.clonedChallenge;
|
||||
store.state.user.data.challenges.push(newChallenge._id);
|
||||
return newChallenge;
|
||||
}
|
||||
|
||||
export async function joinChallenge (store, payload) {
|
||||
let response = await axios.post(`/api/v3/challenges/${payload.challengeId}/join`);
|
||||
|
||||
|
||||
@@ -86,8 +86,3 @@ export async function markChatSeen (store, payload) {
|
||||
let response = await axios.post(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
// @TODO: should this be here?
|
||||
// function clearCards () {
|
||||
// User.user._wrapped && User.set({'flags.cardReceived':false});
|
||||
// }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import omit from 'lodash/omit';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
export async function getPublicGuilds (store, payload) {
|
||||
let params = {
|
||||
@@ -49,12 +50,26 @@ export async function getGroup (store, payload) {
|
||||
|
||||
|
||||
export async function join (store, payload) {
|
||||
let response = await axios.post(`/api/v3/groups/${payload.guildId}/join`);
|
||||
const groupId = payload.groupId;
|
||||
const type = payload.type;
|
||||
const user = store.state.user.data;
|
||||
const invitations = user.invitations;
|
||||
|
||||
// @TODO: abstract for parties as well
|
||||
store.state.user.data.guilds.push(payload.guildId);
|
||||
if (payload.type === 'myGuilds') {
|
||||
let response = await axios.post(`/api/v3/groups/${groupId}/join`);
|
||||
|
||||
if (type === 'guild') {
|
||||
const invitationI = invitations.guilds.findIndex(i => i.id === groupId);
|
||||
if (invitationI !== -1) invitations.guilds.splice(invitationI, 1);
|
||||
|
||||
user.guilds.push(groupId);
|
||||
store.state.myGuilds.push(response.data.data);
|
||||
} else if (type === 'party') {
|
||||
const invitationI = invitations.parties.findIndex(i => i.id === groupId);
|
||||
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
|
||||
|
||||
user.party._id = groupId;
|
||||
|
||||
Analytics.updateUser({partyID: groupId});
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
@@ -111,9 +126,20 @@ export async function update (store, payload) {
|
||||
}
|
||||
|
||||
export async function rejectInvite (store, payload) {
|
||||
let response = await axios.post(`/api/v3/groups/${payload.groupId}/reject-invite`);
|
||||
const groupId = payload.groupId;
|
||||
const type = payload.type;
|
||||
const user = store.state.user.data;
|
||||
const invitations = user.invitations;
|
||||
|
||||
// @TODO: refresh or add guild
|
||||
let response = await axios.post(`/api/v3/groups/${groupId}/reject-invite`);
|
||||
|
||||
if (type === 'guild') {
|
||||
const invitationI = invitations.guilds.findIndex(i => i.id === groupId);
|
||||
if (invitationI !== -1) invitations.guilds.splice(invitationI, 1);
|
||||
} else if (type === 'party') {
|
||||
const invitationI = invitations.parties.findIndex(i => i.id === groupId);
|
||||
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function readNotification (store, payload) {
|
||||
let url = `api/v3/notifications/${payload.notificationId}/read`;
|
||||
let url = `/api/v3/notifications/${payload.notificationId}/read`;
|
||||
let response = await axios.post(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function readNotifications (store, payload) {
|
||||
let url = 'api/v3/notifications/read';
|
||||
let url = '/api/v3/notifications/read';
|
||||
let response = await axios.post(url, {
|
||||
notificationIds: payload.notificationIds,
|
||||
});
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function seeNotification (store, payload) {
|
||||
let url = `/api/v3/notifications/${payload.notificationId}/see`;
|
||||
let response = await axios.post(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function seeNotifications (store, payload) {
|
||||
let url = '/api/v3/notifications/see';
|
||||
let response = await axios.post(url, {
|
||||
notificationIds: payload.notificationIds,
|
||||
});
|
||||
|
||||
@@ -191,6 +191,11 @@ export async function unassignTask (store, payload) {
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function needsWork (store, payload) {
|
||||
let response = await axios.post(`/api/v3/tasks/${payload.taskId}/needs-work/${payload.userId}`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getGroupApprovals (store, payload) {
|
||||
let response = await axios.get(`/api/v3/approvals/group/${payload.groupId}`);
|
||||
return response.data.data;
|
||||
|
||||
@@ -120,6 +120,10 @@ export function openMysteryItem () {
|
||||
return axios.post('/api/v3/user/open-mystery-item');
|
||||
}
|
||||
|
||||
export function newStuffLater () {
|
||||
return axios.post('/api/v3/news/tell-me-later');
|
||||
}
|
||||
|
||||
export async function rebirth () {
|
||||
let result = await axios.post('/api/v3/user/rebirth');
|
||||
|
||||
|
||||
@@ -128,7 +128,6 @@ export default function () {
|
||||
modalStack: [],
|
||||
equipmentDrawerOpen: true,
|
||||
groupPlans: [],
|
||||
groupNotifications: [],
|
||||
isRunningYesterdailies: false,
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user