Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30036963b1 | ||
|
|
e1fe48bee4 | ||
|
|
77b19ffe97 | ||
|
|
e4b2ef6599 | ||
|
|
a26c2bce23 | ||
|
|
783338f1f0 | ||
|
|
4132a39b90 | ||
|
|
36cc229dd2 | ||
|
|
ef0c11c2bd | ||
|
|
3a8312832c | ||
|
|
df17753bb6 | ||
|
|
c9b3c646eb | ||
|
|
cd67877ef3 | ||
|
|
251e1d45af | ||
|
|
1a97d69edd | ||
|
|
7baa7427a0 | ||
|
|
270078a030 | ||
|
|
bb2768071d | ||
|
|
ec75de5a90 | ||
|
|
b9b944ba29 | ||
|
|
fa9553b371 | ||
|
|
8d5cbbe9be | ||
|
|
3a339a4a09 | ||
|
|
324b16b8cd | ||
|
|
8c005aa05a | ||
|
|
664cf5a47b | ||
|
|
71926cff51 | ||
|
|
e971f49c85 | ||
|
|
01657f573d | ||
|
|
95613dcfb8 | ||
|
|
bcf304984d | ||
|
|
08d84ba691 | ||
|
|
4007c26801 | ||
|
|
344c20fd99 | ||
|
|
f5c2c39f6a | ||
|
|
0379f341b9 | ||
|
|
8498ab9fe2 | ||
|
|
bda3fb5f4d | ||
|
|
eff8db0afd | ||
|
|
8cce38ede1 | ||
|
|
ab0dae8df3 | ||
|
|
0824af05b7 | ||
|
|
28bf024990 | ||
|
|
8c59420d4e | ||
|
|
8a30ac0607 | ||
|
|
e1984762b5 | ||
|
|
0360326f41 | ||
|
|
f3f215abea | ||
|
|
feb98a5ac7 | ||
|
|
95de11cf7a | ||
|
|
5bb336cedc | ||
|
|
790614a9d4 | ||
|
|
977968df19 | ||
|
|
3df056105c | ||
|
|
0eb7e10e7f | ||
|
|
0908fa2a48 | ||
|
|
71904d106f | ||
|
|
56040eebaf | ||
|
|
2094a4d4b8 | ||
|
|
2048f3ef76 | ||
|
|
e9163a1bb2 | ||
|
|
54fd910db2 | ||
|
|
3c0cd7067a | ||
|
|
5a473eb0e0 | ||
|
|
e6fcdf62ef | ||
|
|
09e748bc92 | ||
|
|
90532b0763 | ||
|
|
9151690f86 | ||
|
|
c125ac4d93 | ||
|
|
aa61c1fe06 | ||
|
|
c6a7ee3f56 | ||
|
|
411213f381 | ||
|
|
b2dabcaf98 | ||
|
|
2f3927fcaa | ||
|
|
f84562446d | ||
|
|
95c1893b0c | ||
|
|
6fba71ea2c | ||
|
|
5755bfc952 | ||
|
|
08c6e8298c | ||
|
|
5a30b0cf1f | ||
|
|
cf847cd1d8 | ||
|
|
5753d3e648 | ||
|
|
4718e5e5ea | ||
|
|
9a2dbace30 | ||
|
|
ef07abfd28 | ||
|
|
b28adc6b42 | ||
|
|
c814eabb29 | ||
|
|
4dc19a8c60 | ||
|
|
89f6a9b07e | ||
|
|
a23926f34f | ||
|
|
118198b594 | ||
|
|
97e80e2093 | ||
|
|
2588befceb | ||
|
|
f09274225a | ||
|
|
03b66abe70 | ||
|
|
9d41fb0252 | ||
|
|
012fa2f8ef | ||
|
|
00d8d9d0cc | ||
|
|
cc4772c75a | ||
|
|
fabaaa6d92 | ||
|
|
854696728a | ||
|
|
314926cc06 | ||
|
|
68fa834946 | ||
|
|
09440d5adf | ||
|
|
1724bfc553 | ||
|
|
8304f99ecb | ||
|
|
d2fcdf4493 | ||
|
|
649404ac6a | ||
|
|
774db2564f | ||
|
|
521a1e646d | ||
|
|
2619ac37d9 | ||
|
|
2ce9b319a0 | ||
|
|
477c23dd67 | ||
|
|
7af71d1457 | ||
|
|
3171550de2 | ||
|
|
6477801d3e | ||
|
|
14798ced82 | ||
|
|
34d37cefcc | ||
|
|
bd21933cea |
@@ -1,18 +1,5 @@
|
||||
FROM node:10
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
RUN mkdir -p /usr/src/habitrpg
|
||||
WORKDIR /usr/src/habitrpg
|
||||
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN cp config.json.example config.json
|
||||
RUN npm install
|
||||
|
||||
# Create Build dir
|
||||
RUN mkdir -p ./website/build
|
||||
|
||||
# Start Habitica
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
FROM node:10
|
||||
WORKDIR /code
|
||||
COPY package*.json /code/
|
||||
RUN npm install
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
@@ -1,14 +1,45 @@
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "run", "client:dev"]
|
||||
depends_on:
|
||||
- server
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- BASE_URL=http://server:3000
|
||||
image: habitica
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
|
||||
- .:/code
|
||||
- /code/node_modules
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "start"]
|
||||
depends_on:
|
||||
- mongo
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
image: habitica
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
- .:/code
|
||||
- /code/node_modules
|
||||
mongo:
|
||||
image: mongo:3.4
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "27017:27017"
|
||||
networks:
|
||||
habitica:
|
||||
driver: bridge
|
||||
|
||||
@@ -16,7 +16,7 @@ const IMG_DIST_PATH = 'website/client/assets/images/sprites/';
|
||||
const CSS_DIST_PATH = 'website/client/assets/css/sprites/';
|
||||
|
||||
function checkForSpecialTreatment (name) {
|
||||
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame/;
|
||||
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame|^eyewear_special_\w+HalfMoon/;
|
||||
return name.match(regex) || name === 'head_0';
|
||||
}
|
||||
|
||||
|
||||
62
migrations/archive/2019/20190530_halfmoon_glasses.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20190530_halfmoon_glasses';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
'items.gear.owned.eyewear_special_blackHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_blueHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_greenHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_pinkHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_redHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_whiteHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_yellowHalfMoon': true,
|
||||
};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({_id: user._id}, {$set: set}).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2019-05-01')},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = 'mystery_items_201904';
|
||||
const MYSTERY_ITEMS = ['armor_mystery_201904', 'head_mystery_201904'];
|
||||
const MIGRATION_NAME = 'mystery_items_201905';
|
||||
const MYSTERY_ITEMS = ['headAccessory_mystery_201905', 'back_mystery_201905'];
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
import { model as UserNotification } from '../../website/server/models/userNotification';
|
||||
|
||||
|
||||
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "4.93.4",
|
||||
"version": "4.99.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.93.4",
|
||||
"version": "4.99.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@google-cloud/trace-agent": "^3.6.0",
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('auth middleware', () => {
|
||||
describe('auth with headers', () => {
|
||||
it('allows to specify a list of user field that we do not want to load', (done) => {
|
||||
const authWithHeaders = authWithHeadersFactory({
|
||||
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'],
|
||||
userFieldsToExclude: ['items'],
|
||||
});
|
||||
|
||||
req.headers['x-api-user'] = user._id;
|
||||
@@ -27,11 +27,34 @@ describe('auth middleware', () => {
|
||||
|
||||
const userToJSON = res.locals.user.toJSON();
|
||||
expect(userToJSON.items).to.not.exist;
|
||||
expect(userToJSON.flags).to.not.exist;
|
||||
expect(userToJSON.auth.timestamps).to.not.exist;
|
||||
expect(userToJSON.auth).to.exist;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('makes sure some fields are always included', (done) => {
|
||||
const authWithHeaders = authWithHeadersFactory({
|
||||
userFieldsToExclude: [
|
||||
'items', 'auth.timestamps',
|
||||
'preferences', 'notifications', '_id', 'flags', 'auth', // these are always loaded
|
||||
],
|
||||
});
|
||||
|
||||
req.headers['x-api-user'] = user._id;
|
||||
req.headers['x-api-key'] = user.apiToken;
|
||||
|
||||
authWithHeaders(req, res, (err) => {
|
||||
if (err) return done(err);
|
||||
|
||||
const userToJSON = res.locals.user.toJSON();
|
||||
expect(userToJSON.items).to.not.exist;
|
||||
expect(userToJSON.auth.timestamps).to.exist;
|
||||
expect(userToJSON.auth).to.exist;
|
||||
expect(userToJSON.notifications).to.exist;
|
||||
expect(userToJSON.preferences).to.exist;
|
||||
expect(userToJSON._id).to.exist;
|
||||
expect(userToJSON.flags).to.exist;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import moment from 'moment';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import validator from 'validator';
|
||||
import { sleep } from '../../../helpers/api-unit.helper';
|
||||
import { sleep, translationCheck } from '../../../helpers/api-unit.helper';
|
||||
import {
|
||||
SPAM_MESSAGE_LIMIT,
|
||||
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
@@ -271,7 +271,16 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member attacks Wailing Whale for 5.0 damage.` `Wailing Whale attacks party for 7.5 damage.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member attacks Wailing Whale for 5.0 damage. Wailing Whale attacks party for 7.5 damage.`',
|
||||
info: {
|
||||
bossDamage: '7.5',
|
||||
quest: 'whale',
|
||||
type: 'boss_damage',
|
||||
user: 'Participating Member',
|
||||
userDamage: '5.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('applies damage only to participating members of party', async () => {
|
||||
@@ -344,7 +353,10 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledTwice;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`You defeated Wailing Whale! Questing party members receive the rewards of victory.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`You defeated Wailing Whale! Questing party members receive the rewards of victory.`',
|
||||
info: { quest: 'whale', type: 'boss_defeated' },
|
||||
});
|
||||
});
|
||||
|
||||
it('calls finishQuest when boss has <= 0 hp', async () => {
|
||||
@@ -387,7 +399,10 @@ describe('Group Model', () => {
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en'));
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: quest.boss.rage.effect('en'),
|
||||
info: { quest: 'trex_undead', type: 'boss_rage' },
|
||||
});
|
||||
expect(party.quest.progress.hp).to.eql(383.5);
|
||||
expect(party.quest.progress.rage).to.eql(0);
|
||||
});
|
||||
@@ -437,7 +452,10 @@ describe('Group Model', () => {
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en'));
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: quest.boss.rage.effect('en'),
|
||||
info: { quest: 'lostMasterclasser4', type: 'boss_rage' },
|
||||
});
|
||||
expect(party.quest.progress.rage).to.eql(0);
|
||||
|
||||
let drainedUser = await User.findById(participatingMember._id);
|
||||
@@ -488,7 +506,15 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 5 Bars of Soap.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member found 5 Bars of Soap.`',
|
||||
info: {
|
||||
items: { soapBars: 5 },
|
||||
quest: 'atom1',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a chat message if no progress is made', async () => {
|
||||
@@ -499,7 +525,15 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 0 Bars of Soap.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member found 0 Bars of Soap.`',
|
||||
info: {
|
||||
items: { soapBars: 0 },
|
||||
quest: 'atom1',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a chat message if no progress is made on quest with multiple items', async () => {
|
||||
@@ -516,9 +550,15 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Blue Fins/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Fire Coral/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member found 0 Fire Coral, 0 Blue Fins.`',
|
||||
info: {
|
||||
items: { blueFins: 0, fireCoral: 0 },
|
||||
quest: 'dilatoryDistress1',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles collection quests with multiple items', async () => {
|
||||
@@ -535,8 +575,14 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/\d* (Tracks|Broken Twigs)/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch({
|
||||
message: sinon.match(/`Participating Member found/).and(sinon.match(/\d* (Tracks|Broken Twigs)/)),
|
||||
info: {
|
||||
quest: 'evilsanta2',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sends message about victory', async () => {
|
||||
@@ -547,7 +593,10 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledTwice;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`All items found! Party has received their rewards.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`All items found! Party has received their rewards.`',
|
||||
info: { type: 'all_items_found' },
|
||||
});
|
||||
});
|
||||
|
||||
it('calls finishQuest when all items are found', async () => {
|
||||
@@ -718,6 +767,258 @@ describe('Group Model', () => {
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateSystemMessages', () => {
|
||||
it('translate quest_start', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'quest_start',
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate boss_damage', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'boss_damage',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'basilist',
|
||||
userDamage: 15.3,
|
||||
bossDamage: 3.7,
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate boss_dont_attack', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'boss_dont_attack',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'basilist',
|
||||
userDamage: 15.3,
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate boss_rage', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'boss_rage',
|
||||
quest: 'lostMasterclasser3',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate boss_defeated', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'boss_defeated',
|
||||
quest: 'lostMasterclasser3',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate user_found_items', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'user_found_items',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'lostMasterclasser1',
|
||||
items: {
|
||||
ancientTome: 3,
|
||||
forbiddenTome: 2,
|
||||
hiddenTome: 1,
|
||||
},
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate all_items_found', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'all_items_found',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate spell_cast_party', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'spell_cast_party',
|
||||
user: questLeader.profile.name,
|
||||
class: 'wizard',
|
||||
spell: 'earth',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate spell_cast_user', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'spell_cast_user',
|
||||
user: questLeader.profile.name,
|
||||
class: 'special',
|
||||
spell: 'snowball',
|
||||
target: participatingMember.profile.name,
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate quest_cancel', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'quest_cancel',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate quest_abort', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'quest_abort',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate tavern_quest_completed', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'tavern_quest_completed',
|
||||
quest: 'stressbeast',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate tavern_boss_rage_tired', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'tavern_boss_rage_tired',
|
||||
quest: 'stressbeast',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate tavern_boss_rage', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'tavern_boss_rage',
|
||||
quest: 'dysheartener',
|
||||
scene: 'market',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate tavern_boss_desperation', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'tavern_boss_desperation',
|
||||
quest: 'stressbeast',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate claim_task', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'claim_task',
|
||||
user: questLeader.profile.name,
|
||||
task: 'Feed the pet',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toJSONCleanChat', () => {
|
||||
it('shows messages with 1 flag to non-admins', async () => {
|
||||
party.chat = [{
|
||||
flagCount: 1,
|
||||
info: {
|
||||
type: 'quest_start',
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
expect(toJSON.chat.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('shows messages with >= 2 flag to admins', async () => {
|
||||
party.chat = [{
|
||||
flagCount: 3,
|
||||
info: {
|
||||
type: 'quest_start',
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
const admin = new User({'contributor.admin': true});
|
||||
let toJSON = await Group.toJSONCleanChat(party, admin);
|
||||
expect(toJSON.chat.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('doesn\'t show flagged messages to non-admins', async () => {
|
||||
party.chat = [{
|
||||
flagCount: 3,
|
||||
info: {
|
||||
type: 'quest_start',
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
expect(toJSON.chat.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Instance Methods', () => {
|
||||
@@ -1007,20 +1308,22 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('formats message', () => {
|
||||
const chatMessage = party.sendChat('a new message', {
|
||||
_id: 'user-id',
|
||||
profile: { name: 'user name' },
|
||||
contributor: {
|
||||
toObject () {
|
||||
return 'contributor object';
|
||||
const chatMessage = party.sendChat({
|
||||
message: 'a new message', user: {
|
||||
_id: 'user-id',
|
||||
profile: { name: 'user name' },
|
||||
contributor: {
|
||||
toObject () {
|
||||
return 'contributor object';
|
||||
},
|
||||
},
|
||||
},
|
||||
backer: {
|
||||
toObject () {
|
||||
return 'backer object';
|
||||
backer: {
|
||||
toObject () {
|
||||
return 'backer object';
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
);
|
||||
|
||||
const chat = chatMessage;
|
||||
|
||||
@@ -1037,7 +1340,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('formats message as system if no user is passed in', () => {
|
||||
const chat = party.sendChat('a system message');
|
||||
const chat = party.sendChat({message: 'a system message'});
|
||||
|
||||
expect(chat.text).to.eql('a system message');
|
||||
expect(validator.isUUID(chat.id)).to.eql(true);
|
||||
@@ -1052,7 +1355,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('updates users about new messages in party', () => {
|
||||
party.sendChat('message');
|
||||
party.sendChat({message: 'message'});
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
@@ -1066,7 +1369,7 @@ describe('Group Model', () => {
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
group.sendChat('message');
|
||||
group.sendChat({message: 'message'});
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
@@ -1076,7 +1379,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('does not send update to user that sent the message', () => {
|
||||
party.sendChat('message', {_id: 'user-id', profile: { name: 'user' }});
|
||||
party.sendChat({message: 'message', user: {_id: 'user-id', profile: { name: 'user' }}});
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
@@ -1088,7 +1391,7 @@ describe('Group Model', () => {
|
||||
it('skips sending new message notification for guilds with > 5000 members', () => {
|
||||
party.memberCount = 5001;
|
||||
|
||||
party.sendChat('message');
|
||||
party.sendChat({message: 'message'});
|
||||
|
||||
expect(User.update).to.not.be.called;
|
||||
});
|
||||
@@ -1096,7 +1399,7 @@ describe('Group Model', () => {
|
||||
it('skips sending messages to the tavern', () => {
|
||||
party._id = TAVERN_ID;
|
||||
|
||||
party.sendChat('message');
|
||||
party.sendChat({message: 'message'});
|
||||
|
||||
expect(User.update).to.not.be.called;
|
||||
});
|
||||
@@ -1928,7 +2231,7 @@ describe('Group Model', () => {
|
||||
|
||||
await guild.save();
|
||||
|
||||
const groupMessage = guild.sendChat('Test message.');
|
||||
const groupMessage = guild.sendChat({message: 'Test message.'});
|
||||
await groupMessage.save();
|
||||
|
||||
await sleep();
|
||||
|
||||
@@ -149,7 +149,7 @@ describe('POST /group', () => {
|
||||
).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotCreatePublicGuildWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotInviteWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -262,7 +262,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotInviteWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -436,7 +436,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotInviteWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -526,7 +526,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotInviteWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ describe('payments - stripe - #checkout', () => {
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({
|
||||
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.include({
|
||||
code: 401,
|
||||
error: 'Error',
|
||||
message: 'Invalid API Key provided: ****************************1111',
|
||||
message: 'Invalid API Key provided: aaaabbbb********************1111',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -127,7 +127,13 @@ describe('POST /groups/:groupId/quests/abort', () => {
|
||||
members: {},
|
||||
});
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch({
|
||||
message: sinon.match(/aborted the party quest Wail of the Whale.`/),
|
||||
info: {
|
||||
quest: 'whale',
|
||||
type: 'quest_abort',
|
||||
},
|
||||
});
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
@@ -141,7 +141,14 @@ describe('POST /groups/:groupId/quests/cancel', () => {
|
||||
members: {},
|
||||
});
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/cancelled the party quest Wail of the Whale.`/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch({
|
||||
message: sinon.match(/cancelled the party quest Wail of the Whale.`/),
|
||||
info: {
|
||||
quest: 'whale',
|
||||
type: 'quest_cancel',
|
||||
user: sinon.match.any,
|
||||
},
|
||||
});
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
@@ -80,6 +80,13 @@ describe('shared.ops.buy', () => {
|
||||
headAccessory_special_redHeadband: true,
|
||||
headAccessory_special_whiteHeadband: true,
|
||||
headAccessory_special_yellowHeadband: true,
|
||||
eyewear_special_blackHalfMoon: true,
|
||||
eyewear_special_blueHalfMoon: true,
|
||||
eyewear_special_greenHalfMoon: true,
|
||||
eyewear_special_pinkHalfMoon: true,
|
||||
eyewear_special_redHalfMoon: true,
|
||||
eyewear_special_whiteHalfMoon: true,
|
||||
eyewear_special_yellowHalfMoon: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -75,6 +75,13 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
headAccessory_special_redHeadband: true,
|
||||
headAccessory_special_whiteHeadband: true,
|
||||
headAccessory_special_yellowHeadband: true,
|
||||
eyewear_special_blackHalfMoon: true,
|
||||
eyewear_special_blueHalfMoon: true,
|
||||
eyewear_special_greenHalfMoon: true,
|
||||
eyewear_special_pinkHalfMoon: true,
|
||||
eyewear_special_redHalfMoon: true,
|
||||
eyewear_special_whiteHalfMoon: true,
|
||||
eyewear_special_yellowHalfMoon: true,
|
||||
});
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
@@ -209,7 +209,7 @@ describe('shared.ops.purchase', () => {
|
||||
|
||||
it('purchases quest bundles', () => {
|
||||
let startingBalance = user.balance;
|
||||
let clock = sandbox.useFakeTimers(moment('2017-05-20').valueOf());
|
||||
let clock = sandbox.useFakeTimers(moment('2019-05-20').valueOf());
|
||||
let type = 'bundles';
|
||||
let key = 'featheredFriends';
|
||||
let price = 1.75;
|
||||
|
||||
@@ -8,6 +8,7 @@ import mongo from './mongo'; // eslint-disable-line
|
||||
import moment from 'moment';
|
||||
import i18n from '../../website/common/script/i18n';
|
||||
import * as Tasks from '../../website/server/models/task';
|
||||
export { translationCheck } from './translate';
|
||||
|
||||
afterEach((done) => {
|
||||
sandbox.restore();
|
||||
|
||||
@@ -16,3 +16,9 @@ export function translate (key, variables, language) {
|
||||
|
||||
return translatedString;
|
||||
}
|
||||
|
||||
export function translationCheck (translatedString) {
|
||||
expect(translatedString).to.not.be.empty;
|
||||
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
|
||||
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ div
|
||||
banned-account-modal
|
||||
amazon-payments-modal(v-if='!isStaticPage')
|
||||
payments-success-modal
|
||||
sub-cancel-modal-confirm(v-if='isUserLoaded')
|
||||
sub-canceled-modal(v-if='isUserLoaded')
|
||||
snackbars
|
||||
router-view(v-if="!isUserLoggedIn || isStaticPage")
|
||||
template(v-else)
|
||||
@@ -187,6 +189,8 @@ import notifications from 'client/mixins/notifications';
|
||||
import { setup as setupPayments } from 'client/libs/payments';
|
||||
import amazonPaymentsModal from 'client/components/payments/amazonModal';
|
||||
import paymentsSuccessModal from 'client/components/payments/successModal';
|
||||
import subCancelModalConfirm from 'client/components/payments/cancelModalConfirm';
|
||||
import subCanceledModal from 'client/components/payments/canceledModal';
|
||||
|
||||
import spellsMixin from 'client/mixins/spells';
|
||||
import { CONSTANTS, getLocalSetting, removeLocalSetting } from 'client/libs/userlocalManager';
|
||||
@@ -210,6 +214,8 @@ export default {
|
||||
amazonPaymentsModal,
|
||||
bannedAccountModal,
|
||||
paymentsSuccessModal,
|
||||
subCancelModalConfirm,
|
||||
subCanceledModal,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -1,30 +1,48 @@
|
||||
.promo_armoire_backgrounds_201904 {
|
||||
.promo_armoire_backgrounds_201906 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -277px;
|
||||
background-position: 0px -361px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_201904 {
|
||||
.promo_bronze_quest {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -425px;
|
||||
background-position: 0px 0px;
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
}
|
||||
.promo_feathered_friends_bundle {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -620px 0px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_floral_sunshine_potions {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -620px -148px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_halfmoon_glasses {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -509px;
|
||||
width: 279px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_201905 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -620px -296px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -277px;
|
||||
background-position: -903px -296px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.scene_spells {
|
||||
.scene_hiking {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -328px 0px;
|
||||
width: 312px;
|
||||
height: 222px;
|
||||
}
|
||||
.scene_yesterdailies_repeatables {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 327px;
|
||||
height: 276px;
|
||||
background-position: -361px 0px;
|
||||
width: 258px;
|
||||
height: 258px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 556 KiB After Width: | Height: | Size: 561 KiB |
|
Before Width: | Height: | Size: 570 KiB After Width: | Height: | Size: 590 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 293 KiB |
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 114 KiB |
@@ -123,3 +123,87 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#subscription-cancel-modal, #subscription-canceled-modal {
|
||||
.modal-content {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.modal-hidden-footer .modal-body {
|
||||
border-bottom-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
justify-content: center;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 0px;
|
||||
border-top-right-radius: 8px;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom: none;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
border-radius: 50%;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background: $gray-700;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
justify-content: center;
|
||||
border-top: none;
|
||||
|
||||
.small-text {
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 24px;
|
||||
background: white;
|
||||
|
||||
.modal-body-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.btn.btn-primary {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cancel-text {
|
||||
color: $gray-50;
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
.details-block {
|
||||
background: $gray-700;
|
||||
border-radius: 4px;
|
||||
padding: 8px 24px;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auto-renew {
|
||||
margin-top: 16px;
|
||||
color: $orange-10;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
3
website/client/assets/svg/exclamation.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="7" height="38" viewBox="0 0 7 38">
|
||||
<path fill="none" fill-rule="evenodd" stroke="#FFF" stroke-linecap="square" stroke-width="7" d="M3.5 4v17m0 12v1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 209 B |
@@ -87,7 +87,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`skin.${set.keys.join(",skin.")}`)') {{ $t('purchaseAll') }}
|
||||
#hair.section.customize-section(v-if='activeTopPage === "hair"')
|
||||
.row.col-12.sub-menu.text-center
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("color")', :class='{active: activeSubPage === "color"}')
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("color")', :class='{active: activeSubPage === "color", "offset-2": !editing}')
|
||||
strong(v-once) {{$t('color')}}
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("bangs")', :class='{active: activeSubPage === "bangs"}')
|
||||
strong(v-once) {{$t('bangs')}}
|
||||
@@ -139,6 +139,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`hair.base.${baseHair4Keys.join(",hair.base.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.customize-options
|
||||
.head_0.option(v-if="!editing", @click='set({"preferences.hair.base": 0})', :class="[{ active: user.preferences.hair.base === 0 }, 'hair_base_0_' + user.preferences.hair.color]")
|
||||
.option(v-for='option in baseHair1',
|
||||
:class='{active: user.preferences.hair.base === option}')
|
||||
.base.sprite.customize-option(:class="`hair_base_${option}_${user.preferences.hair.color}`", @click='set({"preferences.hair.base": option})')
|
||||
@@ -1125,7 +1126,10 @@ export default {
|
||||
return options;
|
||||
},
|
||||
eyewear () {
|
||||
let keys = ['blackTopFrame', 'blueTopFrame', 'greenTopFrame', 'pinkTopFrame', 'redTopFrame', 'whiteTopFrame', 'yellowTopFrame'];
|
||||
let keys = [
|
||||
'blackTopFrame', 'blueTopFrame', 'greenTopFrame', 'pinkTopFrame', 'redTopFrame', 'whiteTopFrame', 'yellowTopFrame',
|
||||
'blackHalfMoon', 'blueHalfMoon', 'greenHalfMoon', 'pinkHalfMoon', 'redHalfMoon', 'whiteHalfMoon', 'yellowHalfMoon',
|
||||
];
|
||||
let options = keys.map(key => {
|
||||
let newKey = `eyewear_special_${key}`;
|
||||
let option = {};
|
||||
|
||||
@@ -29,13 +29,14 @@
|
||||
.btn.btn-primary(v-if='!group.purchased.plan.dateTerminated && group.purchased.plan.paymentMethod === "Stripe"',
|
||||
@click='showStripeEdit({groupId: group.id})') {{ $t('subUpdateCard') }}
|
||||
.btn.btn-sm.btn-danger(v-if='!group.purchased.plan.dateTerminated',
|
||||
@click='cancelSubscription({group: group})') {{ $t('cancelGroupSub') }}
|
||||
@click='cancelSubscriptionConfirm({group: group})') {{ $t('cancelGroupSub') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import paymentsMixin from 'client/mixins/payments';
|
||||
import { CONSTANTS, getLocalSetting, removeLocalSetting } from 'client/libs/userlocalManager';
|
||||
|
||||
export default {
|
||||
mixins: [paymentsMixin],
|
||||
@@ -45,8 +46,20 @@ export default {
|
||||
group: {},
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.loadGroup();
|
||||
async mounted () {
|
||||
await this.loadGroup();
|
||||
|
||||
let appState = getLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
|
||||
if (appState) {
|
||||
appState = JSON.parse(appState);
|
||||
if (appState.groupPlanCanceled) {
|
||||
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
|
||||
this.$root.$emit('habitica:subscription-canceled', {
|
||||
dateTerminated: this.dateTerminated,
|
||||
isGroup: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
|
||||
@@ -35,7 +35,7 @@ sidebar-section(:title="$t('questDetailsTitle')")
|
||||
.grey-progress-bar
|
||||
.collect-progress-bar(:style="{width: (group.quest.progress.collect[key] / value.count) * 100 + '%'}")
|
||||
strong {{group.quest.progress.collect[key]}} / {{value.count}}
|
||||
div.text-right {{parseFloat(user.party.quest.progress.collectedItems) || 0}} items found
|
||||
div.text-right(v-if='userIsOnQuest') {{parseFloat(user.party.quest.progress.collectedItems) || 0}} items found
|
||||
.boss-info(v-if='questData.boss')
|
||||
.row
|
||||
.col-6
|
||||
|
||||
@@ -6,12 +6,12 @@ div
|
||||
report-flag-modal
|
||||
send-gems-modal
|
||||
b-navbar.topbar.navbar-inverse.static-top(toggleable="lg", type="dark", :class="navbarZIndexClass")
|
||||
b-navbar-brand.brand
|
||||
b-navbar-brand.brand(aria-label="Habitica")
|
||||
.logo.svg-icon.d-none.d-xl-block(v-html="icons.logo")
|
||||
.svg-icon.gryphon.d-xs-block.d-xl-none
|
||||
b-navbar-toggle(target='menu_collapse').menu-toggle
|
||||
.quick-menu.mobile-only.form-inline
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')", :aria-label="$t('sync')")
|
||||
.top-menu-icon.svg-icon(v-html="icons.sync")
|
||||
notification-menu.item-with-icon
|
||||
user-dropdown.item-with-icon
|
||||
@@ -64,13 +64,13 @@ div
|
||||
.top-menu-icon.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
|
||||
span {{ userHourglasses }}
|
||||
.item-with-icon
|
||||
.top-menu-icon.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
|
||||
a.top-menu-icon.svg-icon.gem(:aria-label="$t('gems')", href="#buy-gems" v-html="icons.gem", @click.prevent='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
|
||||
span {{userGems}}
|
||||
.item-with-icon.gold
|
||||
.top-menu-icon.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
|
||||
.top-menu-icon.svg-icon(:aria-label="$t('gold')", v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
|
||||
span {{Math.floor(user.stats.gp * 100) / 100}}
|
||||
.form-inline.desktop-only
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
a.item-with-icon(@click="sync", @keyup.enter="sync", role="link", :aria-label="$t('sync')", tabindex="0", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
.top-menu-icon.svg-icon(v-html="icons.sync")
|
||||
notification-menu.item-with-icon
|
||||
user-dropdown.item-with-icon
|
||||
@@ -290,6 +290,7 @@ div
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&:focus /deep/ .top-menu-icon.svg-icon,
|
||||
&:hover /deep/ .top-menu-icon.svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template lang="pug">
|
||||
menu-dropdown.item-notifications(:right="true", @toggled="handleOpenStatusChange", :openStatus="openStatus")
|
||||
div(slot="dropdown-toggle")
|
||||
div(v-b-tooltip.hover.bottom="$t('notifications')")
|
||||
div(:aria-label="$t('notifications')", v-b-tooltip.hover.bottom="$t('notifications')")
|
||||
message-count(
|
||||
v-if='notificationsCount > 0',
|
||||
:count="notificationsCount",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template lang="pug">
|
||||
menu-dropdown.item-user(:right="true")
|
||||
div(slot="dropdown-toggle")
|
||||
div(v-b-tooltip.hover.bottom="$t('user')")
|
||||
div(:aria-label="$t('user')", v-b-tooltip.hover.bottom="$t('user')")
|
||||
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages", :top="true")
|
||||
.top-menu-icon.svg-icon.user(v-html="icons.user")
|
||||
.user-dropdown(slot="dropdown-content")
|
||||
|
||||
@@ -400,7 +400,7 @@ export default {
|
||||
|
||||
// List of prompts for user on changes. Sounds like we may need a refactor here, but it is clean for now
|
||||
if (!this.user.flags.welcomed) {
|
||||
this.$store.state.avatarEditorOptions.editingUser = false;
|
||||
if (this.$store.state.avatarEditorOptions) this.$store.state.avatarEditorOptions.editingUser = false;
|
||||
return this.$root.$emit('bv::show::modal', 'avatar-modal');
|
||||
}
|
||||
|
||||
|
||||
67
website/client/components/payments/cancelModalConfirm.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template lang="pug">
|
||||
b-modal#subscription-cancel-modal(
|
||||
size='sm',
|
||||
:hideFooter="true",
|
||||
:modalClass="['modal-hidden-footer']"
|
||||
)
|
||||
div(slot="modal-header")
|
||||
.icon-container.warning-container.d-flex.align-items-center.justify-content-center
|
||||
.svg-icon.warning(v-html="icons.warning", v-once)
|
||||
.row
|
||||
.col-12.modal-body-col
|
||||
h2 {{ config && config.group ? $t('cancelGroupSub') : $t('cancelSub') }}
|
||||
span.cancel-text {{ config && config.group ? $t('confirmCancelGroupPlan') : $t('confirmCancelSub') }}
|
||||
button.btn.btn-danger.mt-4.mb-3(v-once, @click="close(); cancelSubscription(config)") {{ $t('cancelSub') }}
|
||||
a.standard-link(v-once, @click="close()") {{ $t('neverMind') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
#subscription-cancel-modal .modal-header {
|
||||
border-top: 8px solid $maroon-100;
|
||||
|
||||
.warning-container {
|
||||
background: $maroon-100;
|
||||
}
|
||||
|
||||
.warning {
|
||||
width: 7px;
|
||||
height: 38px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import warningIcon from 'assets/svg/exclamation.svg';
|
||||
import closeIcon from 'assets/svg/close.svg';
|
||||
import paymentsMixin from 'client/mixins/payments';
|
||||
|
||||
export default {
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
warning: warningIcon,
|
||||
close: closeIcon,
|
||||
}),
|
||||
config: null,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:cancel-subscription-confirm', (config) => {
|
||||
this.config = config;
|
||||
this.$root.$emit('bv::show::modal', 'subscription-cancel-modal');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('habitica:cancel-subscription-confirm');
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'subscription-cancel-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
100
website/client/components/payments/canceledModal.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template lang="pug">
|
||||
b-modal#subscription-canceled-modal(
|
||||
size='sm',
|
||||
:hideFooter="true",
|
||||
:modalClass="['modal-hidden-footer']"
|
||||
)
|
||||
div(slot="modal-header")
|
||||
.svg-icon.close(v-html="icons.close", v-once, @click="close()")
|
||||
.icon-container.check-container.d-flex.align-items-center.justify-content-center
|
||||
.svg-icon.check(v-html="icons.check", v-once)
|
||||
.row
|
||||
.col-12.modal-body-col
|
||||
h2 {{ $t(isGroup ? 'canceledGroupPlan' : 'subCanceledTitle') }}
|
||||
.details-block
|
||||
span
|
||||
| {{ $t('subWillBecomeInactive') }}
|
||||
br
|
||||
strong {{ isGroup ? groupDateTerminated : dateTerminated }}
|
||||
span.auto-renew.small-text(v-once) {{ $t('paymentCanceledDisputes') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
#subscription-canceled-modal .modal-header {
|
||||
border-top: 8px solid #1CA372;
|
||||
|
||||
.check-container {
|
||||
background: #1CA372;
|
||||
}
|
||||
|
||||
.check {
|
||||
width: 35.1px;
|
||||
height: 28px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 20px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: 878190;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#subscription-canceled-modal .modal-body {
|
||||
h2 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.details-block {
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import checkIcon from 'assets/svg/check.svg';
|
||||
import closeIcon from 'assets/svg/close.svg';
|
||||
import paymentsMixin from 'client/mixins/payments';
|
||||
|
||||
export default {
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
check: checkIcon,
|
||||
close: closeIcon,
|
||||
}),
|
||||
groupDateTerminated: null,
|
||||
isGroup: null,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:subscription-canceled', ({dateTerminated, isGroup}) => {
|
||||
this.isGroup = isGroup;
|
||||
if (isGroup) {
|
||||
this.groupDateTerminated = dateTerminated;
|
||||
}
|
||||
this.$root.$emit('bv::show::modal', 'subscription-canceled-modal');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('habitica:subscription-canceled');
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'subscription-canceled-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
div(v-if='hasSubscription')
|
||||
.btn.btn-primary(v-if='canEditCardDetails', @click='showStripeEdit()') {{ $t('subUpdateCard') }}
|
||||
.btn.btn-sm.btn-danger(v-if='canCancelSubscription && !loading', @click='cancelSubscription()') {{ $t('cancelSub') }}
|
||||
.btn.btn-sm.btn-danger(v-if='canCancelSubscription && !loading', @click='cancelSubscriptionConfirm()') {{ $t('cancelSub') }}
|
||||
small(v-if='!canCancelSubscription && !hasCanceledSubscription', v-html='getCancelSubInfo()')
|
||||
|
||||
.subscribe-pay(v-if='!hasSubscription || hasCanceledSubscription')
|
||||
@@ -104,7 +104,6 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import filter from 'lodash/filter';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import min from 'lodash/min';
|
||||
@@ -235,10 +234,6 @@ export default {
|
||||
amount: this.numberOfMysticHourglasses,
|
||||
};
|
||||
},
|
||||
dateTerminated () {
|
||||
if (!this.user.preferences || !this.user.preferences.dateFormat) return this.user.purchased.plan.dateTerminated;
|
||||
return moment(this.user.purchased.plan.dateTerminated).format(this.user.preferences.dateFormat.toUpperCase());
|
||||
},
|
||||
canCancelSubscription () {
|
||||
return (
|
||||
this.user.purchased.plan.paymentMethod !== this.paymentMethods.GOOGLE &&
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
|
||||
.column-background {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 32px;
|
||||
|
||||
&.initial-description {
|
||||
|
||||
@@ -3,7 +3,7 @@ A simplified dropdown component that doesn't rely on buttons as toggles like bo
|
||||
-->
|
||||
|
||||
<template lang="pug">
|
||||
.habitica-menu-dropdown.dropdown(@click="toggleDropdown()", :class="{open: isOpen}")
|
||||
.habitica-menu-dropdown.dropdown(@click="toggleDropdown()", @keypress.enter.space.stop.prevent="toggleDropdown()", role="button", tabindex="0", :class="{open: isOpen}", :aria-pressed="isPressed")
|
||||
.habitica-menu-dropdown-toggle
|
||||
slot(name="dropdown-toggle")
|
||||
.dropdown-menu(:class="{'dropdown-menu-right': right}")
|
||||
@@ -12,10 +12,16 @@ A simplified dropdown component that doesn't rely on buttons as toggles like bo
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
.habitica-menu-dropdown {
|
||||
&:hover,
|
||||
&:focus { // NB focus styles match the hover styles for .svg-icon
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.habitica-menu-dropdown.open {
|
||||
.habitica-menu-dropdown-toggle .svg-icon {
|
||||
color: $white !important;
|
||||
&.open {
|
||||
.habitica-menu-dropdown-toggle .svg-icon {
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -66,6 +72,9 @@ export default {
|
||||
if (this.openStatus !== undefined) return this.openStatus === 1 ? true : false;
|
||||
return this.isDropdownOpen;
|
||||
},
|
||||
isPressed () {
|
||||
return this.isOpen ? 'true' : 'false';
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
document.documentElement.addEventListener('click', this._clickOutListener);
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
.time
|
||||
span.mr-1(v-if='conversation.username') @{{ conversation.username }} •
|
||||
span {{ conversation.date | timeAgo }}
|
||||
div {{conversation.lastMessageText ? conversation.lastMessageText.substring(0, 30) : ''}}
|
||||
div.messagePreview {{ conversation.lastMessageText ? removeTags(parseMarkdown(conversation.lastMessageText)) : '' }}
|
||||
.col-8.messages.d-flex.flex-column.justify-content-between
|
||||
.empty-messages.text-center(v-if='!selectedConversation.key')
|
||||
.svg-icon.envelope(v-html="icons.messageIcon")
|
||||
@@ -209,6 +209,16 @@
|
||||
color: $gray-200;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -218,6 +228,7 @@ import filter from 'lodash/filter';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
||||
import axios from 'axios';
|
||||
@@ -389,7 +400,7 @@ export default {
|
||||
if (this.user.flags.chatRevoked) {
|
||||
return {
|
||||
title: this.$t('PMPlaceholderTitleRevoked'),
|
||||
description: this.$t('PMPlaceholderDescriptionRevoked'),
|
||||
description: this.$t('chatPrivilegesRevoked'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -500,6 +511,15 @@ export default {
|
||||
if (!message.contributor) return;
|
||||
return this.icons[`tier${message.contributor.level}`];
|
||||
},
|
||||
removeTags (html) {
|
||||
let tmp = document.createElement('DIV');
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || '';
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
if (!text) return;
|
||||
return habiticaMarkdown.render(String(text));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
.svg-icon.positive-icon(v-html="icons.positive")
|
||||
button.btn.btn-secondary.positive-icon(v-if='this.userLoggedIn.contributor.admin && !adminToolsLoaded',
|
||||
@click="loadAdminTools()", v-b-tooltip.hover.right="'Admin - Load Tools'")
|
||||
.svg-icon.positive-icon(v-html="icons.edit")
|
||||
.svg-icon.positive-icon(v-html="icons.staff")
|
||||
span(v-if='this.userLoggedIn.contributor.admin && adminToolsLoaded')
|
||||
button.btn.btn-secondary.positive-icon(v-if='!hero.flags || (hero.flags && !hero.flags.chatRevoked)',
|
||||
@click="adminRevokeChat()", v-b-tooltip.hover.bottom="'Admin - Revoke Chat Privileges'")
|
||||
@@ -407,7 +407,7 @@ import megaphone from 'assets/svg/broken-megaphone.svg';
|
||||
import lock from 'assets/svg/lock.svg';
|
||||
import challenge from 'assets/svg/challenge.svg';
|
||||
import member from 'assets/svg/member-icon.svg';
|
||||
import edit from 'assets/svg/edit.svg';
|
||||
import staff from 'assets/svg/tier-staff.svg';
|
||||
|
||||
export default {
|
||||
props: ['userId', 'startingPage'],
|
||||
@@ -430,7 +430,7 @@ export default {
|
||||
challenge,
|
||||
lock,
|
||||
member,
|
||||
edit,
|
||||
staff,
|
||||
}),
|
||||
adminToolsLoaded: false,
|
||||
userIdToMessage: '',
|
||||
|
||||
@@ -5,6 +5,9 @@ export default function markdown (el, {value, oldValue}) {
|
||||
|
||||
if (value) {
|
||||
el.innerHTML = habiticaMarkdown.render(String(value));
|
||||
} else {
|
||||
el.innerHTML = '';
|
||||
}
|
||||
|
||||
el.classList.add('markdown');
|
||||
}
|
||||
@@ -8,13 +8,14 @@ import notificationsMixin from 'client/mixins/notifications';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import { CONSTANTS, setLocalSetting } from 'client/libs/userlocalManager';
|
||||
import pick from 'lodash/pick';
|
||||
import moment from 'moment';
|
||||
|
||||
const habiticaUrl = `${location.protocol}//${location.host}`;
|
||||
|
||||
export default {
|
||||
mixins: [notificationsMixin],
|
||||
computed: {
|
||||
...mapState(['credentials']),
|
||||
...mapState({user: 'user.data', credentials: 'credentials'}),
|
||||
paypalCheckoutLink () {
|
||||
return '/paypal/checkout';
|
||||
},
|
||||
@@ -31,6 +32,10 @@ export default {
|
||||
if (this.subscription.coupon) couponString = `&coupon=${this.subscription.coupon}`;
|
||||
return `/paypal/subscribe?sub=${this.subscription.key}${couponString}`;
|
||||
},
|
||||
dateTerminated () {
|
||||
if (!this.user.preferences || !this.user.preferences.dateFormat) return this.user.purchased.plan.dateTerminated;
|
||||
return moment(this.user.purchased.plan.dateTerminated).format(this.user.preferences.dateFormat.toUpperCase());
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
encodeGift (uuid, gift) {
|
||||
@@ -275,10 +280,10 @@ export default {
|
||||
this.amazonPayments.groupToCreate = null;
|
||||
this.amazonPayments.group = null;
|
||||
},
|
||||
cancelSubscriptionConfirm (config) {
|
||||
this.$root.$emit('habitica:cancel-subscription-confirm', config);
|
||||
},
|
||||
async cancelSubscription (config) {
|
||||
if (config && config.group && !confirm(this.$t('confirmCancelGroupPlan'))) return;
|
||||
if (!confirm(this.$t('sureCancelSub'))) return;
|
||||
|
||||
this.loading = true;
|
||||
|
||||
let group;
|
||||
@@ -286,18 +291,10 @@ export default {
|
||||
group = config.group;
|
||||
}
|
||||
|
||||
let paymentMethod = this.user.purchased.plan.paymentMethod;
|
||||
if (group) {
|
||||
paymentMethod = group.purchased.plan.paymentMethod;
|
||||
}
|
||||
let paymentMethod = group ? group.purchased.plan.paymentMethod : this.user.purchased.plan.paymentMethod;
|
||||
paymentMethod = paymentMethod === 'Amazon Payments' ? 'amazon' : paymentMethod.toLowerCase();
|
||||
|
||||
if (paymentMethod === 'Amazon Payments') {
|
||||
paymentMethod = 'amazon';
|
||||
} else {
|
||||
paymentMethod = paymentMethod.toLowerCase();
|
||||
}
|
||||
|
||||
let queryParams = {
|
||||
const queryParams = {
|
||||
noRedirect: true,
|
||||
};
|
||||
|
||||
@@ -309,9 +306,19 @@ export default {
|
||||
const cancelUrl = `/${paymentMethod}/subscribe/cancel?${encodeParams(queryParams)}`;
|
||||
await axios.get(cancelUrl);
|
||||
|
||||
alert(this.$t('paypalCanceled'));
|
||||
// @TODO: We should probably update the api to return the new sub data eventually.
|
||||
await this.$store.dispatch('user:fetch', {forceLoad: true});
|
||||
if (!config || !config.group) {
|
||||
await this.$store.dispatch('user:fetch', {forceLoad: true});
|
||||
this.$root.$emit('habitica:subscription-canceled', {
|
||||
dateTerminated: this.dateTerminated,
|
||||
isGroup: false,
|
||||
});
|
||||
} else {
|
||||
const appState = {
|
||||
groupPlanCanceled: true,
|
||||
};
|
||||
setLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE, JSON.stringify(appState));
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
HabitRPG's translations are managed on [Transifex](https://www.transifex.com/projects/p/habitrpg/).
|
||||
HabitRPG's translations are managed at http://translate.habitica.com/
|
||||
|
||||
The files in this folder are automatically pulled from Transifex, with
|
||||
The files in this folder are automatically pulled from there, with
|
||||
exception of the original American-English strings which are managed
|
||||
directly through GitHub in `locales/en`.
|
||||
|
||||
When you need to change any text, edit only the files in `locales/en`.
|
||||
Do not edit files in any other locales directory. You do not need to
|
||||
request that your changes be translated; changes are automatically
|
||||
copied to Transifex on a regular basis.
|
||||
copied to the translation website on a regular basis.
|
||||
|
||||
If you want to help with translations, please first read [Guidance for
|
||||
Linguists](http://habitica.fandom.com/wiki/Guidance_for_Linguists) and
|
||||
note especially its information about the [Translations Trello
|
||||
card](https://trello.com/c/SvTsLdRF/12-translations).
|
||||
note especially its information about the [Translations Trello card](https://trello.com/c/SvTsLdRF/12-translations).
|
||||
|
||||
@@ -489,5 +489,21 @@
|
||||
"backgroundHalflingsHouseText": "Halfling's House",
|
||||
"backgroundHalflingsHouseNotes": "Visit a charming Halfling's House.",
|
||||
"backgroundBlossomingDesertText": "Blossoming Desert",
|
||||
"backgroundBlossomingDesertNotes": "Witness a rare superbloom in the Blossoming Desert."
|
||||
"backgroundBlossomingDesertNotes": "Witness a rare superbloom in the Blossoming Desert.",
|
||||
|
||||
"backgrounds052019": "SET 60: Released May 2019",
|
||||
"backgroundDojoText": "Dojo",
|
||||
"backgroundDojoNotes": "Learn new moves in a Dojo.",
|
||||
"backgroundParkWithStatueText": "Park with Statue",
|
||||
"backgroundParkWithStatueNotes": "Follow a flower-lined path through a Park with a Statue.",
|
||||
"backgroundRainbowMeadowText": "Rainbow Meadow",
|
||||
"backgroundRainbowMeadowNotes": "Find the pot of gold where a Rainbow ends in a Meadow.",
|
||||
|
||||
"backgrounds062019": "SET 61: Released June 2019",
|
||||
"backgroundSchoolOfFishText": "School of Fish",
|
||||
"backgroundSchoolOfFishNotes": "Swim among a School of Fish.",
|
||||
"backgroundSeasideCliffsText": "Seaside Cliffs",
|
||||
"backgroundSeasideCliffsNotes": "Stand on a beach with the beauty of Seaside Cliffs above.",
|
||||
"backgroundUnderwaterVentsText": "Underwater Vents",
|
||||
"backgroundUnderwaterVentsNotes": "Take a deep dive down, down to the Underwater Vents."
|
||||
}
|
||||
|
||||
@@ -175,6 +175,8 @@
|
||||
"youCast": "You cast <%= spell %>.",
|
||||
"youCastTarget": "You cast <%= spell %> on <%= target %>.",
|
||||
"youCastParty": "You cast <%= spell %> for the party.",
|
||||
"chatCastSpellParty": "<%= username %> casts <%= spell %> for the party.",
|
||||
"chatCastSpellUser": "<%= username %> casts <%= spell %> on <%= target %>.",
|
||||
"critBonus": "Critical Hit! Bonus: ",
|
||||
"gainedGold": "You gained some Gold",
|
||||
"gainedMana": "You gained some Mana",
|
||||
|
||||
@@ -282,6 +282,8 @@
|
||||
"hatchingPotionRoseQuartz": "Rose Quartz",
|
||||
"hatchingPotionCelestial": "Celestial",
|
||||
"hatchingPotionVeggie": "Garden",
|
||||
"hatchingPotionSunshine": "Sunshine",
|
||||
"hatchingPotionBronze": "Bronze",
|
||||
|
||||
"hatchingPotionNotes": "Pour this on an egg, and it will hatch as a <%= potText(locale) %> pet.",
|
||||
"premiumPotionAddlNotes": "Not usable on quest pet eggs.",
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
"missingNewPassword": "Missing new password.",
|
||||
"invalidEmailDomain": "You cannot register with emails with the following domains: <%= domains %>",
|
||||
"wrongPassword": "Wrong password.",
|
||||
"incorrectDeletePhrase": "Please type <%= magicWord %> in all caps to delete your account.",
|
||||
"incorrectDeletePhrase": "Please type <%= magicWord %> in all capital letters to delete your account.",
|
||||
"notAnEmail": "Invalid email address.",
|
||||
"emailTaken": "Email address is already used in an account.",
|
||||
"newEmailRequired": "Missing new email address.",
|
||||
@@ -283,7 +283,7 @@
|
||||
"passwordResetEmailHtml": "If you requested a password reset for <strong><%= username %></strong> on Habitica, <a href=\"<%= passwordResetLink %>\">click here</a> to set a new one. The link will expire after 24 hours.<br/><br>If you haven't requested a password reset, please ignore this email.",
|
||||
"invalidLoginCredentialsLong": "Uh-oh - your email address / username or password is incorrect.\n- Make sure they are typed correctly. Your username and password are case-sensitive.\n- You may have signed up with Facebook or Google-sign-in, not email so double-check by trying them.\n- If you forgot your password, click \"Forgot Password\".",
|
||||
"invalidCredentials": "There is no account that uses those credentials.",
|
||||
"accountSuspended": "This account, User ID \"<%= userId %>\", has been blocked for breaking the [Community Guidelines](https://habitica.com/static/community-guidelines) or [Terms of Service](https://habitica.com/static/terms). For details or to ask to be unblocked, please email our Community Manager at <%= communityManagerEmail %> or ask your parent or guardian to email them. Please copy your User ID into the email and include your username.",
|
||||
"accountSuspended": "This account, User ID \"<%= userId %>\", has been blocked for breaking the Community Guidelines (https://habitica.com/static/community-guidelines) or Terms of Service (https://habitica.com/static/terms). For details or to ask to be unblocked, please email our Community Manager at <%= communityManagerEmail %> or ask your parent or guardian to email them. Please include your @Username in the email.",
|
||||
"accountSuspendedTitle": "Account has been suspended",
|
||||
"unsupportedNetwork": "This network is not currently supported.",
|
||||
"cantDetachSocial": "Account lacks another authentication method; can't detach this authentication method.",
|
||||
|
||||
@@ -422,6 +422,10 @@
|
||||
"weaponArmoireJugglingBallsNotes": "Habiticans are master multi-taskers, so you should have no trouble keeping all these balls in the air! Increases Intelligence by <%= int %>. Enchanted Armoire: Independent Item.",
|
||||
"weaponArmoireSlingshotText": "Slingshot",
|
||||
"weaponArmoireSlingshotNotes": "Take aim at your red Dailies! Increases Strength by <%= str %>. Enchanted Armoire: Independent Item.",
|
||||
"weaponArmoireNephriteBowText": "Nephrite Bow",
|
||||
"weaponArmoireNephriteBowNotes": "This bow shoots special jade-tipped arrows that will take down even your most stubborn bad habits! Increases Intelligence by <%= int %> and Strength by <%= str %>. Enchanted Armoire: Nephrite Archer Set (Item 1 of 3).",
|
||||
"weaponArmoireBambooCaneText": "Bamboo Cane",
|
||||
"weaponArmoireBambooCaneNotes": "Perfect for assisting you in a stroll, or for dancing the Charleston. Increases Intelligence, Perception, and Constitution by <%= attrs %> each. Enchanted Armoire: Boating Set (Item 3 of 3).",
|
||||
|
||||
"armor": "armor",
|
||||
"armorCapitalized": "Armor",
|
||||
@@ -912,6 +916,10 @@
|
||||
"armorArmoireChefsJacketNotes": "This thick cotton jacket is double-breasted to protect you from spills (and conveniently reversible…). Increases Intelligence by <%= int %>. Enchanted Armoire: Chef Set (Item 2 of 4).",
|
||||
"armorArmoireVernalVestmentText": "Vernal Vestment",
|
||||
"armorArmoireVernalVestmentNotes": "This silky garment is perfect for enjoying mild spring weather in style. Increases Strength and Intelligence by <%= attrs %> each. Enchanted Armoire: Vernal Vestments Set (Item 2 of 3).",
|
||||
"armorArmoireNephriteArmorText": "Nephrite Armor",
|
||||
"armorArmoireNephriteArmorNotes": "Made from strong steel rings and decorated with jade, this armor will protect you from procrastination! Increases Strength by <%= str %> and Perception by <%= per %>. Enchanted Armoire: Nephrite Archer Set (Item 3 of 3).",
|
||||
"armorArmoireBoatingJacketText": "Boating Jacket",
|
||||
"armorArmoireBoatingJacketNotes": "Whether you're on a swanky yacht or in a jalopy, you'll be the cat's meow in this jacket and tie. Increases Strength, Intelligence, and Perception by <%= attrs %> each. Enchanted Armoire: Boating Set (Item 1 of 3).",
|
||||
|
||||
"headgear": "helm",
|
||||
"headgearCapitalized": "Headgear",
|
||||
@@ -1416,6 +1424,10 @@
|
||||
"headArmoireVernalHenninNotes": "More than just a pretty hat, this conical chapeau can also hold a rolled-up To-Do list inside. Increases Perception by <%= per %>. Enchanted Armoire: Vernal Vestments Set (Item 1 of 3).",
|
||||
"headArmoireTricornHatText": "Tricorn Hat",
|
||||
"headArmoireTricornHatNotes": "Become a revolutionary jokester! Increases Perception by <%= per %>. Enchanted Armoire: Independent Item.",
|
||||
"headArmoireNephriteHelmText": "Nephrite Helm",
|
||||
"headArmoireNephriteHelmNotes": "The carved jade plume atop this helm is enchanted to enhance your aim. Increases Perception by <%= per %> and Intelligence by <%= int %>. Enchanted Armoire: Nephrite Archer Set (Item 2 of 3).",
|
||||
"headArmoireBoaterHatText": "Boater Hat",
|
||||
"headArmoireBoaterHatNotes": "This straw chapeau is the bee's knees! Increases Strength, Constitution, and Perception by <%= attrs %> each. Enchanted Armoire: Boating Set (Item 2 of 3).",
|
||||
|
||||
"offhand": "off-hand item",
|
||||
"offhandCapitalized": "Off-Hand Item",
|
||||
@@ -1732,6 +1744,8 @@
|
||||
"backMystery201812Notes": "Your luxurious tail shimmers like an icicle, bobbing happily as you pad softly over the snowdrifts. Confers no benefit. December 2018 Subscriber Item.",
|
||||
"backMystery201805Text": "Phenomenal Peacock Tail",
|
||||
"backMystery201805Notes": "This gorgeous feathery tail is perfect for a strut down a lovely garden path! Confers no benefit. May 2018 Subscriber Item.",
|
||||
"backMystery201905Text": "Dazzling Dragon Wings",
|
||||
"backMystery201905Notes": "Fly to untold realms with these iridescent wings. Confers no benefit. May 2019 Subscriber Item.",
|
||||
|
||||
"backSpecialWonderconRedText": "Mighty Cape",
|
||||
"backSpecialWonderconRedNotes": "Swishes with strength and beauty. Confers no benefit. Special Edition Convention Item.",
|
||||
@@ -1900,6 +1914,8 @@
|
||||
"headAccessoryMystery201804Notes": "These fuzzy sound-catchers will ensure you never miss the rustle of a leaf or the sound of an acorn falling! Confers no benefit. April 2018 Subscriber Item.",
|
||||
"headAccessoryMystery201812Text": "Arctic Fox Ears",
|
||||
"headAccessoryMystery201812Notes": "You hear the subtle sound of snowflakes falling upon the landscape. Confers no benefit. December 2018 Subscriber Item.",
|
||||
"headAccessoryMystery201905Text": "Dazzling Dragon Horns",
|
||||
"headAccessoryMystery201905Notes": "These horns are as sharp as they are shimmery. Confers no benefit. May 2019 Subscriber Item.",
|
||||
"headAccessoryMystery301405Text": "Headwear Goggles",
|
||||
"headAccessoryMystery301405Notes": "\"Goggles are for your eyes,\" they said. \"Nobody wants goggles that you can only wear on your head,\" they said. Hah! You sure showed them! Confers no benefit. August 3015 Subscriber Item.",
|
||||
|
||||
@@ -1928,6 +1944,21 @@
|
||||
"eyewearSpecialYellowTopFrameText": "Yellow Standard Eyeglasses",
|
||||
"eyewearSpecialYellowTopFrameNotes": "Glasses with a yellow frame above the lenses. Confers no benefit.",
|
||||
|
||||
"eyewearSpecialBlackHalfMoonText": "Black Half-Moon Eyeglasses",
|
||||
"eyewearSpecialBlackHalfMoonNotes": "Glasses with a black frame and crescent lenses. Confers no benefit.",
|
||||
"eyewearSpecialBlueHalfMoonText": "Blue Half-Moon Eyeglasses",
|
||||
"eyewearSpecialBlueHalfMoonNotes": "Glasses with a blue frame and crescent lenses. Confers no benefit.",
|
||||
"eyewearSpecialGreenHalfMoonText": "Green Half-Moon Eyeglasses",
|
||||
"eyewearSpecialGreenHalfMoonNotes": "Glasses with a green frame and crescent lenses. Confers no benefit.",
|
||||
"eyewearSpecialPinkHalfMoonText": "Pink Half-Moon Eyeglasses",
|
||||
"eyewearSpecialPinkHalfMoonNotes": "Glasses with a pink frame and crescent lenses. Confers no benefit.",
|
||||
"eyewearSpecialRedHalfMoonText": "Red Half-Moon Eyeglasses",
|
||||
"eyewearSpecialRedHalfMoonNotes": "Glasses with a red frame and crescent lenses. Confers no benefit.",
|
||||
"eyewearSpecialWhiteHalfMoonText": "White Half-Moon Eyeglasses",
|
||||
"eyewearSpecialWhiteHalfMoonNotes": "Glasses with a white frame and crescent lenses. Confers no benefit.",
|
||||
"eyewearSpecialYellowHalfMoonText": "Yellow Half-Moon Eyeglasses",
|
||||
"eyewearSpecialYellowHalfMoonNotes": "Glasses with a yellow frame and crescent lenses. Confers no benefit.",
|
||||
|
||||
"eyewearSpecialAetherMaskText": "Aether Mask",
|
||||
"eyewearSpecialAetherMaskNotes": "This mask has a mysterious history. Increases Intelligence by <%= int %>.",
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"continue": "Continue",
|
||||
"accept": "Accept",
|
||||
"reject": "Reject",
|
||||
"neverMind": "Never mind",
|
||||
"neverMind": "Nevermind",
|
||||
"buyMoreGems": "Buy More Gems",
|
||||
"notEnoughGems": "Not enough Gems",
|
||||
"alreadyHave": "Whoops! You already have this item. No need to buy it again!",
|
||||
|
||||
@@ -137,7 +137,6 @@
|
||||
"PMPlaceholderTitle": "Nothing Here Yet",
|
||||
"PMPlaceholderDescription": "Select a conversation on the left",
|
||||
"PMPlaceholderTitleRevoked": "Your chat privileges have been revoked",
|
||||
"PMPlaceholderDescriptionRevoked": "You are not able to send private messages because your chat privileges have been revoked. If you have questions or concerns about this, please email <a href=\"mailto:admin@habitica.com\">admin@habitica.com</a> to discuss it with the staff.",
|
||||
"PMReceive": "Receive Private Messages",
|
||||
"PMEnabledOptPopoverText": "Private Messages are enabled. Users can contact you via your profile.",
|
||||
"PMDisabledOptPopoverText": "Private Messages are disabled. Enable this option to allow users to contact you via your profile.",
|
||||
@@ -266,9 +265,7 @@
|
||||
"userRequestsApproval": "<%= userName %> requests approval",
|
||||
"userCountRequestsApproval": "<%= userCount %> members request approval",
|
||||
"youAreRequestingApproval": "You are requesting approval",
|
||||
"chatPrivilegesRevoked": "You cannot do that because your chat privileges have been revoked.",
|
||||
"cannotCreatePublicGuildWhenMuted": "You cannot create a public guild because your chat privileges have been revoked.",
|
||||
"cannotInviteWhenMuted": "You cannot invite anyone to a guild or party because your chat privileges have been revoked.",
|
||||
"chatPrivilegesRevoked": "You cannot do this because your chat privileges have been removed. For details or to ask if your privileges can be returned, please email our Community Manager at admin@habitica.com or ask your parent or guardian to email them. Please include your @Username in the email. If a moderator has already told you that your chat ban is temporary, you do not need to send an email.",
|
||||
"newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!",
|
||||
"newChatMessageTitle": "New message in <%= groupName %>",
|
||||
"exportInbox": "Export Messages",
|
||||
@@ -348,8 +345,8 @@
|
||||
"leaderCannotLeaveGroupWithActiveGroup": "A leader can not leave a group while the group has an active plan",
|
||||
"youHaveGroupPlan": "You have a free subscription because you are a member of a group that has a Group Plan. This will end when you are no longer in the group that has a Group Plan. Any months of extra subscription credit you have will be applied at the end of the Group Plan.",
|
||||
"cancelGroupSub": "Cancel Group Plan",
|
||||
"confirmCancelGroupPlan": "Are you sure you want to cancel the group plan and remove its benefits from all members, including their free subscriptions?",
|
||||
"canceledGroupPlan": "Canceled Group Plan",
|
||||
"confirmCancelGroupPlan": "Are you sure you want to cancel your Group Plan? All Group members will lose their subscription and benefits.",
|
||||
"canceledGroupPlan": "Group Plan Canceled",
|
||||
"groupPlanCanceled": "Group Plan will become inactive on",
|
||||
"purchasedGroupPlanPlanExtraMonths": "You have <%= months %> months of extra group plan credit.",
|
||||
"addManager": "Assign Manager",
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"messageGroupChatFlagAlreadyReported": "You have already reported this message",
|
||||
"messageGroupChatNotFound": "Message not found!",
|
||||
"messageGroupChatAdminClearFlagCount": "Only an admin can clear the flag count!",
|
||||
"messageCannotFlagSystemMessages": "You cannot flag a system message. If you need to report a violation of the Community Guidelines related to this message, please email a screenshot and explanation to Lemoness at <%= communityManagerEmail %>.",
|
||||
"messageCannotFlagSystemMessages": "You cannot report a system message. If you need to report a violation of the Community Guidelines related to this message, please email a screenshot and explanation to our Community Manager at <%= communityManagerEmail %>.",
|
||||
"messageGroupChatSpam": "Whoops, looks like you're posting too many messages! Please wait a minute and try again. The Tavern chat only holds 200 messages at a time, so Habitica encourages posting longer, more thoughtful messages and consolidating replies. Can't wait to hear what you have to say. :)",
|
||||
"messageCannotLeaveWhileQuesting": "You cannot accept this party invitation while you are in a quest. If you'd like to join this party, you must first abort your quest, which you can do from your party screen. You will be given back the quest scroll.",
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
"paymentYouSentSubscription": "You sent <strong><%= name %></strong> a <%= months %>-months Habitica subscription.",
|
||||
"paymentSubBilling": "Your subscription will be billed <strong>$<%= amount %></strong> every <strong><%= months %> months</strong>.",
|
||||
"paymentAutoRenew": "This subscription will auto-renew until it is canceled. If you need to cancel this subscription, you can do so from your settings.",
|
||||
"paymentCanceledDisputes": "We’ve sent a cancelation confirmation to your email. If you don’t see the email, please contact us to prevent future billing disputes.",
|
||||
"success": "Success!",
|
||||
|
||||
"classGear": "Class Gear",
|
||||
|
||||