Compare commits

..

4 Commits

Author SHA1 Message Date
SabreCat
08127cfa67 4.270.0 2023-05-09 10:25:24 -05:00
SabreCat
ae9f10420f Merge branch 'sabrecat/link-tweak' into release 2023-05-09 10:25:19 -05:00
SabreCat
26bddb6383 Merge branch 'release' into 2023-05-backgrounds-and-armoire 2023-05-09 10:05:11 -05:00
CuriousMagpie
9a157168a9 feat(content): add May Backgrounds and Enchanted Armoire items 2023-05-03 12:41:42 -04:00
398 changed files with 6682 additions and 11844 deletions

View File

@@ -87,5 +87,5 @@
"REDIS_HOST": "aaabbbcccdddeeefff",
"REDIS_PORT": "1234",
"REDIS_PASSWORD": "12345678",
"TRUSTED_DOMAINS": "localhost,habitica.com"
"TRUSTED_DOMAINS": "https://localhost,https://habitica.com"
}

View File

@@ -1,158 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230522_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Parrot-Base']
&& pets['Parrot-CottonCandyBlue']
&& pets['Parrot-CottonCandyPink']
&& pets['Parrot-Desert']
&& pets['Parrot-Golden']
&& pets['Parrot-Red']
&& pets['Parrot-Shade']
&& pets['Parrot-Skeleton']
&& pets['Parrot-White']
&& pets['Parrot-Zombie']
&& pets['Rooster-Base']
&& pets['Rooster-CottonCandyBlue']
&& pets['Rooster-CottonCandyPink']
&& pets['Rooster-Desert']
&& pets['Rooster-Golden']
&& pets['Rooster-Red']
&& pets['Rooster-Shade']
&& pets['Rooster-Skeleton']
&& pets['Rooster-White']
&& pets['Rooster-Zombie']
&& pets['Triceratops-Base']
&& pets['Triceratops-CottonCandyBlue']
&& pets['Triceratops-CottonCandyPink']
&& pets['Triceratops-Desert']
&& pets['Triceratops-Golden']
&& pets['Triceratops-Red']
&& pets['Triceratops-Shade']
&& pets['Triceratops-Skeleton']
&& pets['Triceratops-White']
&& pets['Triceratops-Zombie']
&& pets['TRex-Base']
&& pets['TRex-CottonCandyBlue']
&& pets['TRex-CottonCandyPink']
&& pets['TRex-Desert']
&& pets['TRex-Golden']
&& pets['TRex-Red']
&& pets['TRex-Shade']
&& pets['TRex-Skeleton']
&& pets['TRex-White']
&& pets['TRex-Zombie']
&& pets['Pterodactyl-Base']
&& pets['Pterodactyl-CottonCandyBlue']
&& pets['Pterodactyl-CottonCandyPink']
&& pets['Pterodactyl-Desert']
&& pets['Pterodactyl-Golden']
&& pets['Pterodactyl-Red']
&& pets['Pterodactyl-Shade']
&& pets['Pterodactyl-Skeleton']
&& pets['Pterodactyl-White']
&& pets['Pterodactyl-Zombie']
&& pets['Owl-Base']
&& pets['Owl-CottonCandyBlue']
&& pets['Owl-CottonCandyPink']
&& pets['Owl-Desert']
&& pets['Owl-Golden']
&& pets['Owl-Red']
&& pets['Owl-Shade']
&& pets['Owl-Skeleton']
&& pets['Owl-White']
&& pets['Owl-Zombie']
&& pets['Velociraptor-Base']
&& pets['Velociraptor-CottonCandyBlue']
&& pets['Velociraptor-CottonCandyPink']
&& pets['Velociraptor-Desert']
&& pets['Velociraptor-Golden']
&& pets['Velociraptor-Red']
&& pets['Velociraptor-Shade']
&& pets['Velociraptor-Skeleton']
&& pets['Velociraptor-White']
&& pets['Velociraptor-Zombie']
&& pets['Penguin-Base']
&& pets['Penguin-CottonCandyBlue']
&& pets['Penguin-CottonCandyPink']
&& pets['Penguin-Desert']
&& pets['Penguin-Golden']
&& pets['Penguin-Red']
&& pets['Penguin-Shade']
&& pets['Penguin-Skeleton']
&& pets['Penguin-White']
&& pets['Penguin-Zombie']
&& pets['Falcon-Base']
&& pets['Falcon-CottonCandyBlue']
&& pets['Falcon-CottonCandyPink']
&& pets['Falcon-Desert']
&& pets['Falcon-Golden']
&& pets['Falcon-Red']
&& pets['Falcon-Shade']
&& pets['Falcon-Skeleton']
&& pets['Falcon-White']
&& pets['Falcon-Zombie']
&& pets['Peacock-Base']
&& pets['Peacock-CottonCandyBlue']
&& pets['Peacock-CottonCandyPink']
&& pets['Peacock-Desert']
&& pets['Peacock-Golden']
&& pets['Peacock-Red']
&& pets['Peacock-Shade']
&& pets['Peacock-Skeleton']
&& pets['Peacock-White']
&& pets['Peacock-Zombie']) {
set['achievements.dinosaurDynasty'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-04-15') },
};
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]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,79 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230718_summer_splash_orcas';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = { migration: MIGRATION_NAME };
const push = {};
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
return;
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
set['items.pets.Orca-Base'] = 5;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_orca_pet',
title: 'Orcas for Summer Splash!',
text: 'To celebrate Summer Splash, we\'ve given you an Orca Pet!',
destination: 'stable',
},
seen: false,
};
} else {
set['items.mounts.Orca-Base'] = true;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_orca_mount',
title: 'Orcas for Summer Splash!',
text: 'To celebrate Summer Splash, we\'ve given you an Orca Mount!',
destination: 'stable',
},
seen: false,
};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await user.updateOne({ $set: set, $push: push }).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2023-06-18')},
};
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)
.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
}
};

View File

@@ -1,155 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230731_naming_day';
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++;
let set;
let push;
const inc = {
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Red': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_Skeleton': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Zombie': 1,
'achievements.habiticaDays': 1,
};
if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.back_special_namingDay2020 !== 'undefined') {
set = { migration: MIGRATION_NAME };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_cake',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you some cake!',
destination: '/inventory/items',
},
seen: false,
},
};
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.body_special_namingDay2018 !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.back_special_namingDay2020': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_back',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Tail and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.body_special_namingDay2018': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_body',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Cloak and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.head_special_namingDay2017': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_head',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Helm and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.pets.Gryphon-RoyalPurple': 5 };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_pet',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Pet and cake!',
destination: '/inventory/stable',
},
seen: false,
},
};
} else {
set = { migration: MIGRATION_NAME, 'items.mounts.Gryphon-RoyalPurple': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_mount',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Mount and cake!',
destination: '/inventory/stable',
},
seen: false,
},
};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return await user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
} else {
return await user.updateOne({ $set: set, $inc: inc }).exec();
}
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-07-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)
.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]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,72 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
import { model as Group } from '../../../website/server/models/group';
const guildsPerRun = 500;
const progressCount = 1000;
const guildsQuery = {
type: 'guild',
};
let count = 0;
async function updateGroup (guild) {
count++;
if (count % progressCount === 0) {
console.warn(`${count} ${guild._id}`);
}
if (guild.hasActiveGroupPlan()) {
return console.warn(`Guild ${guild._id} is active Group Plan`);
}
const leader = await User
.findOne({ _id: guild.leader })
.select({ _id: true })
.exec();
if (!leader) {
return console.warn(`Leader not found for Guild ${guild._id}`);
}
if (guild.balance > 0) {
await leader.updateBalance(
guild.balance,
'create_guild',
'',
`Guild Bank refund for ${guild.name} (${guild._id})`,
);
}
return guild.updateOne({ $set: { balance: 0 } }).exec();
}
export default async function processGroups () {
const guildFields = {
_id: 1,
balance: 1,
leader: 1,
name: 1,
purchased: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const foundGroups = await Group // eslint-disable-line no-await-in-loop
.find(guildsQuery)
.limit(guildsPerRun)
.sort({ _id: 1 })
.select(guildFields)
.exec();
if (foundGroups.length === 0) {
console.warn('All appropriate Guilds found and modified.');
console.warn(`\n${count} Guilds processed\n`);
break;
} else {
guildsQuery._id = {
$gt: foundGroups[foundGroups.length - 1],
};
}
await Promise.all(foundGroups.map(guild => updateGroup(guild))); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,62 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
import { TransactionModel as Transaction } from '../../../website/server/models/transaction';
const transactionsPerRun = 500;
const progressCount = 1000;
const transactionsQuery = {
transactionType: 'create_guild',
amount: { $gt: 0 },
};
let count = 0;
async function updateTransaction (transaction) {
count++;
if (count % progressCount === 0) {
console.warn(`${count} ${transaction._id}`);
}
const leader = await User
.findOne({ _id: transaction.userId })
.select({ _id: true })
.exec();
if (!leader) {
return console.warn(`User not found for transaction ${transaction._id}`);
}
return leader.updateOne(
{ $inc: { balance: transaction.amount }},
).exec();
}
export default async function processTransactions () {
const transactionFields = {
_id: 1,
userId: 1,
currency: 1,
amount: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const foundTransactions = await Transaction // eslint-disable-line no-await-in-loop
.find(transactionsQuery)
.limit(transactionsPerRun)
.sort({ _id: 1 })
.select(transactionFields)
.lean()
.exec();
if (foundTransactions.length === 0) {
console.warn('All appropriate transactions found and modified.');
console.warn(`\n${count} transactions processed\n`);
break;
} else {
transactionsQuery._id = {
$gt: foundTransactions[foundTransactions.length - 1],
};
}
await Promise.all(foundTransactions.map(txn => updateTransaction(txn))); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,144 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230808_veteran_pet_ladder';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push = { notifications: { $each: [] }};
set.migration = MIGRATION_NAME;
if (user.items.pets['Fox-Veteran']) {
set['items.pets.Dragon-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_dragon',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Dragon.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Bear-Veteran']) {
set['items.pets.Fox-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_fox',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Fox.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Lion-Veteran']) {
set['items.pets.Bear-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_bear',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Bear.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Tiger-Veteran']) {
set['items.pets.Lion-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_lion',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Lion.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Wolf-Veteran']) {
set['items.pets.Tiger-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_tiger',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Tiger.',
destination: '/inventory/stable',
},
seen: false,
});
} else {
set['items.pets.Wolf-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_wolf',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Wolf.',
destination: '/inventory/stable',
},
seen: false,
});
}
if (user.contributor.level > 0) {
set['items.gear.owned.armor_special_heroicTunic'] = true;
set['items.gear.owned.back_special_heroicAureole'] = true;
set['items.gear.owned.headAccessory_special_heroicCirclet'] = true;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'heroic_set_icon',
title: 'Youve received the Heroic Set!',
text: 'To commemorate your hard work as a contributor, weve awarded you the Heroic Circlet, Heroic Aureole, and Heroic Tunic.',
destination: '/inventory/equipment',
},
seen: false,
});
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
// 'auth.timestamps.loggedin': { $gt: new Date('2023-07-08') },
};
const fields = {
_id: 1,
items: 1,
migration: 1,
contributor: 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
}
};

2907
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.0.1",
"version": "4.270.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@babel/register": "^7.22.5",
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@babel/register": "^7.18.9",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.1.3",
"@slack/webhook": "^6.1.0",
@@ -14,9 +14,9 @@
"amazon-payments": "^0.2.9",
"amplitude": "^6.0.0",
"apidoc": "^0.54.0",
"apple-auth": "^1.0.9",
"apple-auth": "^1.0.7",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"body-parser": "^1.20.1",
"bootstrap": "^4.6.0",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
@@ -42,7 +42,7 @@
"image-size": "^1.0.2",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.1.5",
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
@@ -67,16 +67,16 @@
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"stripe": "^12.9.0",
"superagent": "^8.0.9",
"stripe": "^11.10.0",
"superagent": "^8.0.6",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.9.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.9.0",
"winston": "^3.8.2",
"winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.6.0"
"xml2js": "^0.4.23"
},
"private": true,
"engines": {
@@ -110,7 +110,7 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"axios": "^1.3.6",
"axios": "^1.2.2",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
@@ -122,7 +122,7 @@
"monk": "^7.3.4",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon": "^15.1.2",
"sinon": "^15.0.1",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
},

View File

@@ -748,19 +748,9 @@ describe('payments/index', () => {
});
it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
});
it('resets plans.consecutive.offset if 1 month subscription', async () => {
user.purchased.plan.consecutive.offset = 1;
await user.save();
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
expect(user.purchased.plan.extraMonths).to.eql(0);
});
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {

View File

@@ -242,7 +242,7 @@ describe('cron middleware', () => {
sandbox.spy(cronLib, 'recoverCron');
sandbox.stub(User, 'updateOne')
sandbox.stub(User, 'update')
.withArgs({
_id: user._id,
$or: [

View File

@@ -1732,7 +1732,7 @@ describe('Group Model', () => {
});
it('updates participting members (not including user)', async () => {
sandbox.spy(User, 'updateMany');
sandbox.spy(User, 'update');
await party.startQuest(nonParticipatingMember);
@@ -1740,7 +1740,7 @@ describe('Group Model', () => {
questLeader._id, participatingMember._id, sleepingParticipatingMember._id,
];
expect(User.updateMany).to.be.calledWith(
expect(User.update).to.be.calledWith(
{ _id: { $in: members } },
{
$set: {
@@ -1753,11 +1753,11 @@ describe('Group Model', () => {
});
it('updates non-user quest leader and decrements quest scroll', async () => {
sandbox.spy(User, 'updateOne');
sandbox.spy(User, 'update');
await party.startQuest(participatingMember);
expect(User.updateOne).to.be.calledWith(
expect(User.update).to.be.calledWith(
{ _id: questLeader._id },
{
$inc: {
@@ -1819,29 +1819,29 @@ describe('Group Model', () => {
};
it('doesn\'t retry successful operations', async () => {
sandbox.stub(User, 'updateOne').returns(successfulMock);
sandbox.stub(User, 'update').returns(successfulMock);
await party.finishQuest(quest);
expect(User.updateOne).to.be.calledThrice;
expect(User.update).to.be.calledThrice;
});
it('stops retrying when a successful update has occurred', async () => {
const updateStub = sandbox.stub(User, 'updateOne');
const updateStub = sandbox.stub(User, 'update');
updateStub.onCall(0).returns(failedMock);
updateStub.returns(successfulMock);
await party.finishQuest(quest);
expect(User.updateOne.callCount).to.equal(4);
expect(User.update.callCount).to.equal(4);
});
it('retries failed updates at most five times per user', async () => {
sandbox.stub(User, 'updateOne').returns(failedMock);
sandbox.stub(User, 'update').returns(failedMock);
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.updateOne.callCount).to.eql(15); // for 3 users
expect(User.update.callCount).to.eql(15); // for 3 users
});
});
@@ -2088,17 +2088,17 @@ describe('Group Model', () => {
context('Party quests', () => {
it('updates participating members with rewards', async () => {
sandbox.spy(User, 'updateOne');
sandbox.spy(User, 'update');
await party.finishQuest(quest);
expect(User.updateOne).to.be.calledThrice;
expect(User.updateOne).to.be.calledWithMatch({
expect(User.update).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({
_id: questLeader._id,
});
expect(User.updateOne).to.be.calledWithMatch({
expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id,
});
expect(User.updateOne).to.be.calledWithMatch({
expect(User.update).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id,
});
});
@@ -2173,11 +2173,11 @@ describe('Group Model', () => {
});
it('updates all users with rewards', async () => {
sandbox.spy(User, 'updateMany');
sandbox.spy(User, 'update');
await party.finishQuest(tavernQuest);
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({});
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({});
});
it('sets quest completed to the world quest key', async () => {

View File

@@ -16,7 +16,60 @@ describe('GET /challenges/:challengeId', () => {
});
});
context('Group Plan', () => {
context('public guild', () => {
let groupLeader;
let group;
let challenge;
let user;
beforeEach(async () => {
user = await generateUser();
const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'public' },
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('should return challenge data', async () => {
await challenge.sync();
const chal = await user.get(`/challenges/${challenge._id}`);
expect(chal.memberCount).to.equal(challenge.memberCount);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: { name: groupLeader.profile.name },
auth: {
local: {
username: groupLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(chal.group).to.eql({
_id: group._id,
categories: [],
id: group.id,
name: group.name,
summary: group.name,
type: group.type,
privacy: group.privacy,
leader: groupLeader.id,
});
});
});
context('private guild', () => {
let groupLeader;
let challengeLeader;
let group;
@@ -31,14 +84,14 @@ describe('GET /challenges/:challengeId', () => {
const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
members: 2,
upgradeToGroupPlan: true,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
[challengeLeader, otherMember] = members;
challengeLeader = members[0]; // eslint-disable-line prefer-destructuring
otherMember = members[1]; // eslint-disable-line prefer-destructuring
challenge = await generateChallenge(challengeLeader, group);
});

View File

@@ -71,18 +71,42 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('populates only some fields', async () => {
const group = await generateGroup(user, { type: 'party', privacy: 'private', name: generateUUID() });
const challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
it('works with challenges belonging to public guild', async () => {
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(leader, group);
await leader.post(`/challenges/${challenge._id}/join`);
const res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: user._id,
id: user._id,
profile: { name: user.profile.name },
_id: leader._id,
id: leader._id,
profile: { name: leader.profile.name },
auth: {
local: {
username: user.auth.local.username,
username: leader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
it('populates only some fields', async () => {
const anotherUser = await generateUser({ balance: 3 });
const group = await generateGroup(anotherUser, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(anotherUser, group);
await anotherUser.post(`/challenges/${challenge._id}/join`);
const res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: anotherUser._id,
id: anotherUser._id,
profile: { name: anotherUser.profile.name },
auth: {
local: {
username: anotherUser.auth.local.username,
},
},
flags: {

View File

@@ -72,6 +72,20 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
});
});
it('works with challenges belonging to a public guild', async () => {
const groupLeader = await generateUser({ balance: 4 });
const group = await generateGroup(groupLeader, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
const taskText = 'Test Text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{ type: 'habit', text: taskText }]);
const memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
expect(memberProgress).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile', 'tasks']);
expect(memberProgress.profile).to.have.all.keys(['name']);
expect(memberProgress.tasks.length).to.equal(1);
});
it('returns the member tasks for the challenges', async () => {
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
const challenge = await generateChallenge(user, group);

View File

@@ -7,7 +7,117 @@ import {
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/groups/:groupId', () => {
context('Group Plan', () => {
context('Public Guild', () => {
let publicGuild; let user; let nonMember; let challenge; let
challenge2;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
});
publicGuild = group;
user = groupLeader;
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return group challenges for non member with populated leader', async () => {
const challenges = await nonMember.get(`/challenges/groups/${publicGuild._id}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('should return group challenges for member with populated leader', async () => {
const challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('should return newest challenges first', async () => {
let challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
expect(foundChallengeIndex).to.eql(0);
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
});
context('Private Guild', () => {
let privateGuild; let user; let nonMember; let challenge; let
challenge2;
@@ -18,7 +128,6 @@ describe('GET challenges/groups/:groupId', () => {
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
privateGuild = group;
@@ -77,6 +186,68 @@ describe('GET challenges/groups/:groupId', () => {
});
});
context('official challenge is present', () => {
let publicGuild; let user; let officialChallenge; let unofficialChallenges;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
});
user = groupLeader;
publicGuild = group;
await user.update({
'permissions.challengeAdmin': true,
});
officialChallenge = await generateChallenge(user, group, {
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
});
await user.post(`/challenges/${officialChallenge._id}/join`);
// We add 10 extra challenges to test whether the official challenge
// (the oldest) makes it to the front page.
unofficialChallenges = [];
for (let i = 0; i < 10; i += 1) {
const challenge = await generateChallenge(user, group); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
unofficialChallenges.push(challenge);
}
});
it('should return official challenges first', async () => {
const challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
it('should return newest challenges first, after official ones', async () => {
let challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
unofficialChallenges.forEach((chal, index) => {
const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id });
expect(foundChallengeIndex).to.eql(10 - index);
});
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(1);
});
});
context('Party', () => {
let party; let user; let nonMember; let challenge; let
challenge2;
@@ -230,7 +401,7 @@ describe('GET challenges/groups/:groupId', () => {
});
});
it('should return tavern challenges using ID "habitrpg"', async () => {
it('should return tavern challenges using ID "habitrpg', async () => {
const challenges = await user.get('/challenges/groups/habitrpg');
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
@@ -264,58 +435,5 @@ describe('GET challenges/groups/:groupId', () => {
},
});
});
context('official challenge is present', () => {
let officialChallenge; let unofficialChallenges;
before(async () => {
await user.update({
'permissions.challengeAdmin': true,
balance: 3,
});
officialChallenge = await generateChallenge(user, tavern, {
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
prize: 1,
});
await user.post(`/challenges/${officialChallenge._id}/join`);
// We add 10 extra challenges to test whether the official challenge
// (the oldest) makes it to the front page.
unofficialChallenges = [];
for (let i = 0; i < 10; i += 1) {
const challenge = await generateChallenge(user, tavern, { prize: 1 }); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
unofficialChallenges.push(challenge);
}
});
it('should return official challenges first', async () => {
const challenges = await user.get('/challenges/groups/habitrpg');
const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
it('should return newest challenges first, after official ones', async () => {
let challenges = await user.get('/challenges/groups/habitrpg');
unofficialChallenges.forEach((chal, index) => {
const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id });
expect(foundChallengeIndex).to.eql(10 - index);
});
const newChallenge = await generateChallenge(user, tavern, { prize: 1 });
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/groups/habitrpg');
const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(1);
});
});
});
});

View File

@@ -2,44 +2,39 @@ import {
generateUser,
generateChallenge,
createAndPopulateGroup,
resetHabiticaDB,
} from '../../../../helpers/api-integration/v3';
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/user', () => {
context('no official challenges', () => {
let user; let member; let nonMember; let challenge; let challenge2; let publicChallenge;
let groupPlan; let userData; let groupData; let tavern; let tavernData;
let user; let member; let nonMember; let challenge; let challenge2;
let publicGuild; let userData; let groupData;
before(async () => {
await resetHabiticaDB();
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
upgradeToGroupPlan: true,
});
groupPlan = group;
publicGuild = group;
groupData = {
_id: groupPlan._id,
_id: publicGuild._id,
categories: [],
id: groupPlan._id,
type: groupPlan.type,
privacy: groupPlan.privacy,
name: groupPlan.name,
summary: groupPlan.name,
leader: groupPlan.leader._id,
id: publicGuild._id,
type: publicGuild.type,
privacy: publicGuild.privacy,
name: publicGuild.name,
summary: publicGuild.name,
leader: publicGuild.leader._id,
};
user = groupLeader;
userData = {
_id: groupPlan.leader._id,
id: groupPlan.leader._id,
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
@@ -51,31 +46,17 @@ describe('GET challenges/user', () => {
},
};
tavern = await user.get(`/groups/${TAVERN_ID}`);
tavernData = {
_id: TAVERN_ID,
categories: [],
id: TAVERN_ID,
type: tavern.type,
privacy: tavern.privacy,
name: tavern.name,
summary: tavern.name,
leader: tavern.leader._id,
};
member = members[0]; // eslint-disable-line prefer-destructuring
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
challenge2 = await generateChallenge(user, group);
await user.update({ balance: 0.25 });
publicChallenge = await generateChallenge(user, tavern, { prize: 1 });
await member.post(`/challenges/${challenge._id}/join`);
await nonMember.post(`/challenges/${challenge._id}/join`);
});
context('all challenges', () => {
it('should return challenges user has joined', async () => {
const challenges = await member.get('/challenges/user?page=0');
const challenges = await nonMember.get('/challenges/user?page=0');
const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.exist;
@@ -83,13 +64,11 @@ describe('GET challenges/user', () => {
expect(foundChallenge.group).to.eql(groupData);
});
it('should return public challenges', async () => {
it('should not return challenges a non-member has not joined', async () => {
const challenges = await nonMember.get('/challenges/user?page=0');
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
});
it('should return challenges user has created', async () => {
@@ -121,10 +100,10 @@ describe('GET challenges/user', () => {
it('should return newest challenges first', async () => {
let challenges = await user.get('/challenges/user?page=0');
let foundChallengeIndex = _.findIndex(challenges, { _id: publicChallenge._id });
let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
expect(foundChallengeIndex).to.eql(0);
const newChallenge = await generateChallenge(user, groupPlan);
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user?page=0');
@@ -134,23 +113,52 @@ describe('GET challenges/user', () => {
});
it('should not return challenges user doesn\'t have access to', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
const privateChallenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
const challenges = await nonMember.get('/challenges/user?page=0');
const foundChallenge = _.find(challenges, { _id: challenge._id });
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
it('should not return challenges user doesn\'t have access to, even with query parameters', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
const privateChallenge = await generateChallenge(groupLeader, group, {
categories: [{
name: 'academics',
slug: 'academics',
}],
});
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
const challenges = await nonMember.get('/challenges/user?page=0&categories=academics&owned=not_owned');
const foundChallenge = _.find(challenges, { _id: challenge._id });
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
});
context('my challenges', () => {
it('should return challenges user has joined', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const challenges = await nonMember.get(`/challenges/user?page=0&member=${true}`);
const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.exist;
@@ -169,10 +177,6 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
});
it('should return challenges user has created if filter by owned', async () => {
@@ -186,10 +190,6 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
});
it('should not return challenges user has created if filter by not owned', async () => {
@@ -199,40 +199,36 @@ describe('GET challenges/user', () => {
expect(foundChallenge1).to.not.exist;
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.not.exist;
});
it('should not return challenges in user groups', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.not.exist;
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
});
it('should not return public challenges', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.not.exist;
});
});
});
context('official challenge is present', () => {
let user; let officialChallenge; let unofficialChallenges; let
group;
publicGuild;
before(async () => {
({ group, groupLeader: user } = await createAndPopulateGroup({
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
summary: 'summary for TestGuild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
upgradeToGroupPlan: true,
}));
});
user = groupLeader;
publicGuild = group;
await user.update({
'permissions.challengeAdmin': true,
@@ -275,7 +271,7 @@ describe('GET challenges/user', () => {
}
});
const newChallenge = await generateChallenge(user, group);
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user?page=0');
@@ -298,10 +294,9 @@ describe('GET challenges/user', () => {
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
upgradeToGroupPlan: true,
});
user = groupLeader;

View File

@@ -42,7 +42,26 @@ describe('POST /challenges', () => {
});
});
it('returns error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
it('returns error when creating a challenge in a public guild and you are not a member of it', async () => {
const user = await generateUser();
const { group } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
});
await expect(user.post('/challenges', {
group: group._id,
prize: 4,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustBeGroupMember'),
});
});
it('return error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
const user = await generateUser();
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
const group = createAndPopulateGroup({
@@ -58,7 +77,7 @@ describe('POST /challenges', () => {
});
});
context('creating a Challenge for a Group Plan', () => {
context('Creating a challenge for a valid group', () => {
let groupLeader;
let group;
let groupMember;
@@ -75,11 +94,9 @@ describe('POST /challenges', () => {
challenges: true,
},
},
upgradeToGroupPlan: true,
});
groupLeader = await populatedGroup.groupLeader.sync();
await groupLeader.update({ permissions: {} });
group = populatedGroup.group;
groupMember = populatedGroup.members[0]; // eslint-disable-line prefer-destructuring
});

View File

@@ -18,7 +18,6 @@ describe('PUT /challenges/:challengeId', () => {
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
});
privateGuild = group;

View File

@@ -1,6 +1,7 @@
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -9,30 +10,27 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
admin;
before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
privacy: 'public',
},
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
members: 2,
upgradeToGroupPlan: true,
});
groupWithChat = group;
user = groupLeader;
message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message;
userThatDidNotCreateChat = members[0]; // eslint-disable-line prefer-destructuring
admin = members[1]; // eslint-disable-line prefer-destructuring
await admin.update({ permissions: { moderator: true } });
userThatDidNotCreateChat = await generateUser();
admin = await generateUser({ 'permissions.moderator': true });
});
context('Chat errors', () => {
it('returns an error if message does not exist', async () => {
it('returns an error is message does not exist', async () => {
const fakeChatId = generateUUID();
await expect(user.del(`/groups/${groupWithChat._id}/chat/${fakeChatId}`)).to.eventually.be.rejected.and.eql({
code: 404,
@@ -58,7 +56,7 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
nextMessage = nextMessage.message;
});
it('allows creator to delete their message', async () => {
it('allows creator to delete a their message', async () => {
await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);

View File

@@ -1,6 +1,6 @@
import {
generateUser,
createAndPopulateGroup,
generateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -11,22 +11,48 @@ describe('GET /groups/:groupId/chat', () => {
user = await generateUser();
});
context('public Guild', () => {
let group;
before(async () => {
const leader = await generateUser({ balance: 2 });
group = await generateGroup(leader, {
name: 'test group',
type: 'guild',
privacy: 'public',
}, {
chat: [
{ text: 'Hello', flags: {}, id: 1 },
{ text: 'Welcome to the Guild', flags: {}, id: 2 },
],
});
});
it('returns Guild chat', async () => {
const chat = await user.get(`/groups/${group._id}/chat`);
expect(chat[0].id).to.eql(group.chat[0].id);
expect(chat[1].id).to.eql(group.chat[1].id);
});
});
context('private Guild', () => {
let group;
before(async () => {
({ group } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
const leader = await generateUser({ balance: 2 });
group = await generateGroup(leader, {
name: 'test group',
type: 'guild',
privacy: 'private',
}, {
chat: [
'Hello',
'Welcome to the Guild',
],
}));
});
});
it('returns error if user is not member of requested private group', async () => {

View File

@@ -1,42 +1,32 @@
import find from 'lodash/find';
import { find } from 'lodash';
import moment from 'moment';
import nconf from 'nconf';
import { IncomingWebhook } from '@slack/webhook';
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat/:chatId/flag', () => {
let user; let admin; let anotherUser; let newUser; let
group; let members; let userToDelete;
group;
const TEST_MESSAGE = 'Test Message';
const USER_AGE_FOR_FLAGGING = 3;
beforeEach(async () => {
({ group, groupLeader: user, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'private',
},
leaderDetails: {
'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate(),
},
members: 4,
upgradeToGroupPlan: true,
}));
[admin, anotherUser, newUser, userToDelete] = members;
await user.update({ permissions: {} });
await admin.update({ permissions: { moderator: true } });
await anotherUser.update({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
await newUser.update({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() });
await userToDelete.update({
'auth.timestamps.created': moment().subtract(1, 'days').toDate(),
'purchased.plan.dateTerminated': moment().subtract(1, 'minutes').toDate(),
});
user = await generateUser({ balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
admin = await generateUser({ balance: 1, 'permissions.moderator': true });
anotherUser = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
newUser = await generateUser({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() });
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
group = await user.post('/groups', {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
});
});
afterEach(() => {
@@ -79,8 +69,8 @@ describe('POST /chat/:chatId/flag', () => {
fallback: 'Flag Message',
color: 'danger',
author_name: `@${anotherUser.auth.local.username} ${anotherUser.profile.name} (${anotherUser.auth.local.email}; ${anotherUser._id})\n${timestamp}`,
title: 'Flag in Test Guild - (private guild)',
title_link: undefined,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.>`,
mrkdwn_in: [
@@ -88,7 +78,7 @@ describe('POST /chat/:chatId/flag', () => {
],
}],
});
/* eslint-enable camelcase */
/* eslint-ensable camelcase */
});
it('Does not increment message flag count and sends different message to moderator Slack when user is new', async () => {
@@ -114,8 +104,8 @@ describe('POST /chat/:chatId/flag', () => {
fallback: 'Flag Message',
color: 'danger',
author_name: `@${newUser.auth.local.username} ${newUser.profile.name} (${newUser.auth.local.email}; ${newUser._id})\n${timestamp}`,
title: 'Flag in Test Guild - (private guild)',
title_link: undefined,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.> ${automatedComment}`,
mrkdwn_in: [
@@ -123,12 +113,15 @@ describe('POST /chat/:chatId/flag', () => {
],
}],
});
/* eslint-enable camelcase */
/* eslint-ensable camelcase */
});
it('Flags a chat when the author\'s account was deleted', async () => {
const { message } = await userToDelete.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await userToDelete.del('/user', {
const deletedUser = await generateUser({
'auth.timestamps.created': new Date('2022-01-01'),
});
const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await deletedUser.del('/user', {
password: 'password',
});

View File

@@ -6,27 +6,27 @@ import {
describe('POST /chat/:chatId/like', () => {
let user;
let anotherUser;
let groupWithChat;
let members;
const testMessage = 'Test Message';
let anotherUser;
before(async () => {
({ group: groupWithChat, groupLeader: user, members } = await createAndPopulateGroup({
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
upgradeToGroupPlan: true,
}));
});
[anotherUser] = members;
user = groupLeader;
groupWithChat = group;
anotherUser = members[0]; // eslint-disable-line prefer-destructuring
await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});

View File

@@ -1,33 +1,41 @@
import { IncomingWebhook } from '@slack/webhook';
import nconf from 'nconf';
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
translate as t,
sleep,
server,
} from '../../../../helpers/api-integration/v3';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
import { CHAT_FLAG_FROM_SHADOW_MUTE, MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
import * as email from '../../../../../website/server/libs/email';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat', () => {
let user; let groupWithChat; let member; let
additionalMember;
const testMessage = 'Test Message';
const testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE';
const testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1';
const testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE';
const testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1';
const bannedWordErrorMessage = t('bannedWordUsed', { swearWordsUsed: testBannedWordMessage });
before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 2,
upgradeToGroupPlan: true,
});
user = groupLeader;
await user.update({
@@ -35,7 +43,8 @@ describe('POST /chat', () => {
'auth.timestamps.created': new Date('2022-01-01'),
}); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group;
[member, additionalMember] = members;
member = members[0]; // eslint-disable-line prefer-destructuring
additionalMember = members[1]; // eslint-disable-line prefer-destructuring
await member.update({ 'auth.timestamps.created': new Date('2022-01-01') });
await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
@@ -80,12 +89,32 @@ describe('POST /chat', () => {
member.update({ 'flags.chatRevoked': false });
});
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
const userWithChatRevoked = await member.update({ 'flags.chatRevoked': true });
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('does not error when chat privileges are revoked when sending a message to a private guild', async () => {
await member.update({
'flags.chatRevoked': true,
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const privateGuildMemberWithChatsRevoked = members[0];
await privateGuildMemberWithChatsRevoked.update({
'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
});
@@ -123,12 +152,54 @@ describe('POST /chat', () => {
member.update({ 'flags.chatShadowMuted': false });
});
it('creates a chat with flagCount already set and notifies mods when sending a message to a public guild', async () => {
const userWithChatShadowMuted = await member.update({ 'flags.chatShadowMuted': true });
const message = await userWithChatShadowMuted.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.eql('shadow-muted-post-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${member.auth.local.username} / ${member.profile.name} posted while shadow-muted`,
attachments: [{
fallback: 'Shadow-Muted Message',
color: 'danger',
author_name: `@${member.auth.local.username} ${member.profile.name} (${member.auth.local.email}; ${member._id})`,
title: 'Shadow-Muted Post in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testMessage,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});
it('creates a chat with zero flagCount when sending a message to a private guild', async () => {
await member.update({
'flags.chatShadowMuted': true,
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({
'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
@@ -155,9 +226,100 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
it('creates a chat with zero flagCount when non-shadow-muted user sends a message to a public guild', async () => {
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
});
context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('returns an error when chat message contains a banned word in a public guild', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
await expect(members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is part of a phrase', async () => {
const wordInPhrase = `phrase ${testBannedWordMessage} end`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is surrounded by non alphabet characters', async () => {
const wordInPhrase = `_!${testBannedWordMessage}@_`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is typed in mixed case', async () => {
const substrLength = Math.floor(testBannedWordMessage.length / 2);
const chatMessage = testBannedWordMessage.substring(0, substrLength).toLowerCase()
+ testBannedWordMessage.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed', { swearWordsUsed: chatMessage }),
});
});
it('checks error message has all the banned words used, regardless of case', async () => {
const testBannedWords = [
testBannedWordMessage.toUpperCase(),
testBannedWordMessage1.toLowerCase(),
];
const chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`;
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected
.and.have.property('message')
.that.includes(testBannedWords.join(', '));
});
it('does not error when bad word is suffix of a word', async () => {
const wordAsSuffix = `prefix${testBannedWordMessage}`;
const message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix });
expect(message.message.id).to.exist;
});
it('does not error when bad word is prefix of a word', async () => {
const wordAsPrefix = `${testBannedWordMessage}suffix`;
const message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix });
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a party', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
@@ -174,8 +336,37 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a public guild in which banned words are allowed', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
// Update the bannedWordsAllowed property for the group
group.update({ bannedWordsAllowed: true });
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a private guild', async () => {
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testBannedWordMessage });
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'private guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
expect(message.message.id).to.exist;
});
@@ -192,6 +383,45 @@ describe('POST /chat', () => {
user.update({ 'flags.chatRevoked': false });
});
it('errors and revokes privileges when chat message contains a banned slur', async () => {
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testSlurMessage })).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.eql('slur-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${user.profile.name} (${user.id}) tried to post a slur`,
attachments: [{
fallback: 'Slur Message',
color: 'danger',
author_name: `@${user.auth.local.username} ${user.profile.name} (${user.auth.local.email}; ${user._id})`,
title: 'Slur in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testSlurMessage,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
// Chat privileges are revoked
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('allows slurs in private groups', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
@@ -207,17 +437,28 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
});
it('errors when slur is typed in mixed case', async () => {
const substrLength = Math.floor(testSlurMessage1.length / 2);
const chatMessage = testSlurMessage1.substring(0, substrLength).toLowerCase()
+ testSlurMessage1.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
});
});
it('errors when user account is too young', async () => {
await user.update({ 'auth.timestamps.created': new Date() });
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: 'hi im new' }))
const brandNewUser = await generateUser();
await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('chatTemporarilyUnavailable'),
});
await user.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('creates a chat', async () => {
@@ -278,42 +519,54 @@ describe('POST /chat', () => {
const mount = 'test-mount';
const pet = 'test-pet';
const style = 'test-style';
await user.update({
const userWithStyle = await generateUser({
'items.currentMount': mount,
'items.currentPet': pet,
'preferences.style': style,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.currentMount).to.eql(user.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(user.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(user.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(user.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(user.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(user.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(user.preferences.chair);
expect(message.message.userStyles.items.currentMount).to.eql(userWithStyle.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(userWithStyle.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(userWithStyle.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(userWithStyle.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(userWithStyle.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(userWithStyle.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(userWithStyle.preferences.chair);
expect(message.message.userStyles.preferences.background)
.to.eql(user.preferences.background);
.to.eql(userWithStyle.preferences.background);
});
it('creates equipped to user styles', async () => {
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const userWithStyle = await generateUser({
'preferences.costume': false,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.gear.equipped)
.to.eql(user.items.gear.equipped);
.to.eql(userWithStyle.items.gear.equipped);
expect(message.message.userStyles.items.gear.costume).to.not.exist;
});
it('creates costume to user styles', async () => {
await user.update({ 'preferences.costume': true });
const userWithStyle = await generateUser({
'preferences.costume': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.gear.costume).to.eql(user.items.gear.costume);
expect(message.message.userStyles.items.gear.costume).to.eql(userWithStyle.items.gear.costume);
expect(message.message.userStyles.items.gear.equipped).to.not.exist;
});
@@ -323,11 +576,12 @@ describe('POST /chat', () => {
tier: 800,
tokensApplied: true,
};
await user.update({
const backer = await generateUser({
backer: backerInfo,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const messageBackerInfo = message.message.backer;
expect(messageBackerInfo.npc).to.equal(backerInfo.npc);
@@ -407,5 +661,43 @@ describe('POST /chat', () => {
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
expect(memberWithNotification.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === group._id)).to.exist;
});
it('does not notify other users of a new message that is already hidden from shadow-muting', async () => {
await user.update({ 'flags.chatShadowMuted': true });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const memberWithNotification = await member.get('/user');
await user.update({ 'flags.chatShadowMuted': false });
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.not.exist;
expect(memberWithNotification.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id)).to.not.exist;
});
});
context('Spam prevention', () => {
it('Returns an error when the user has been posting too many messages', async () => {
// Post as many messages are needed to reach the spam limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i += 1) {
const result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatSpam'),
});
});
it('contributor should not receive spam alert', async () => {
const userSocialite = await member.update({ 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL });
// Post 1 more message than the spam limit to ensure they do not reach the limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i += 1) {
const result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
});
});
});

View File

@@ -12,19 +12,18 @@ describe('POST /groups/:id/chat/seen', () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
upgradeToGroupPlan: true,
});
guild = group;
guildLeader = groupLeader;
[guildMember] = members;
guildMember = members[0]; // eslint-disable-line prefer-destructuring
guildMessage = await guildLeader.post(`/groups/${guild._id}/chat`, { message: 'Some guild message' });
guildMessage = guildMessage.message;

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import config from '../../../../../config.json';
@@ -12,24 +13,21 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
admin;
before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
privacy: 'public',
},
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
upgradeToGroupPlan: true,
members: 2,
});
groupWithChat = group;
author = groupLeader;
[nonAdmin, admin] = members;
await nonAdmin.update({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
await admin.update({ 'permissions.moderator': true });
nonAdmin = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
admin = await generateUser({ 'permissions.moderator': true });
message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message;

View File

@@ -1,5 +1,6 @@
import {
createAndPopulateGroup,
generateUser,
generateGroup,
} from '../../../../helpers/api-integration/v3';
describe('GET /group-plans', () => {
@@ -7,15 +8,20 @@ describe('GET /group-plans', () => {
let groupPlan;
before(async () => {
({ group: groupPlan, groupLeader: user } = await createAndPopulateGroup({
groupDetails: {
name: 'group plan - is member',
user = await generateUser({ balance: 4 });
groupPlan = await generateGroup(user,
{
name: 'public guild - is member',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
upgradeToGroupPlan: true,
leaderDetails: { balance: 4 },
}));
{
purchased: {
plan: {
customerId: 'existings',
},
},
});
});
it('returns group plans for the user', async () => {

View File

@@ -1,63 +1,70 @@
import {
createAndPopulateGroup,
generateUser,
resetHabiticaDB,
generateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import apiError from '../../../../../website/server/libs/apiError';
describe('GET /groups', () => {
let user; let leader; let members;
let secondGroup; let secondLeader;
const NUMBER_OF_USERS_PRIVATE_GUILDS = 2;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 3;
let user;
let userInGuild;
const NUMBER_OF_PUBLIC_GUILDS = 2;
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER = 2;
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
const GUILD_PER_PAGE = 30;
const categories = [{
slug: 'newCat',
name: 'New Category',
}];
let publicGuildNotMember;
let privateGuildUserIsMemberOf;
before(async () => {
await resetHabiticaDB();
({
group: privateGuildUserIsMemberOf,
groupLeader: leader,
members,
} = await createAndPopulateGroup({
groupDetails: {
name: 'private guild - is member',
type: 'guild',
privacy: 'private',
categories,
},
leaderDetails: {
balance: 10,
},
members: 1,
upgradeToGroupPlan: true,
}));
[user] = members;
await user.update({ balance: 4 });
const leader = await generateUser({ balance: 10 });
user = await generateUser({ balance: 4 });
({ group: secondGroup, groupLeader: secondLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'c++ coders',
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
}));
const publicGuildUserIsMemberOf = await generateGroup(leader, {
name: 'public guild - is member',
type: 'guild',
privacy: 'public',
summary: 'ohayou kombonwa',
description: 'oyasumi',
});
await leader.post(`/groups/${publicGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] });
await user.post(`/groups/${publicGuildUserIsMemberOf._id}/join`);
await secondLeader.post(`/groups/${secondGroup._id}/invite`, { uuids: [user._id] });
await user.post(`/groups/${secondGroup._id}/join`);
userInGuild = await generateUser({ guilds: [publicGuildUserIsMemberOf._id] });
await createAndPopulateGroup({
groupDetails: {
name: 'private guild - is not member',
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
publicGuildNotMember = await generateGroup(leader, {
name: 'public guild - is not member',
type: 'guild',
privacy: 'public',
summary: 'Natsume Soseki',
description: 'Kinnosuke no Hondana',
categories,
});
privateGuildUserIsMemberOf = await generateGroup(leader, {
name: 'private guild - is member',
type: 'guild',
privacy: 'private',
categories,
});
await leader.post(`/groups/${privateGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] });
await user.post(`/groups/${privateGuildUserIsMemberOf._id}/join`);
await generateGroup(leader, {
name: 'private guild - is not member',
type: 'guild',
privacy: 'private',
});
await generateGroup(leader, {
@@ -91,16 +98,172 @@ describe('GET /groups', () => {
});
});
it('returns only the tavern when tavern passed in as query', async () => {
await expect(user.get('/groups?type=tavern'))
.to.eventually.have.a.lengthOf(1)
.and.to.have.nested.property('[0]')
.and.to.have.property('_id', TAVERN_ID);
});
it('returns only the user\'s party when party passed in as query', async () => {
await expect(user.get('/groups?type=party'))
.to.eventually.have.a.lengthOf(1)
.and.to.have.nested.property('[0]');
});
it('returns all public guilds when publicGuilds passed in as query', async () => {
await expect(user.get('/groups?type=publicGuilds'))
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS);
});
describe('filters', () => {
it('returns public guilds filtered by category', async () => {
const guilds = await user.get(`/groups?type=publicGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(publicGuildNotMember._id);
});
it('returns private guilds filtered by category', async () => {
const guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id);
});
it('filters public guilds by size', async () => {
await generateGroup(user, {
name: 'guild1',
type: 'guild',
privacy: 'public',
memberCount: 1,
});
// @TODO: anyway to set higher memberCount in tests right now?
const guilds = await user.get('/groups?type=publicGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
it('filters private guilds by size', async () => {
await generateGroup(user, {
name: 'guild1',
type: 'guild',
privacy: 'private',
memberCount: 1,
});
// @TODO: anyway to set higher memberCount in tests right now?
const guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
it('filters public guilds by leader role', async () => {
const guilds = await user.get('/groups?type=publicGuilds&leader=true');
expect(guilds.length).to.equal(NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER);
});
it('filters public guilds by member role', async () => {
const guilds = await userInGuild.get('/groups?type=publicGuilds&member=true');
expect(guilds.length).to.equal(1);
expect(guilds[0].name).to.have.string('is member');
});
it('filters public guilds by single-word search term', async () => {
const guilds = await user.get('/groups?type=publicGuilds&search=kom');
expect(guilds.length).to.equal(1);
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
});
it('filters public guilds by single-word search term left and right-padded by spaces', async () => {
const guilds = await user.get('/groups?type=publicGuilds&search=++++ohayou+kombonwa+++++');
expect(guilds.length).to.equal(1);
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
});
it('filters public guilds by two-words search term separated by multiple spaces', async () => {
const guilds = await user.get('/groups?type=publicGuilds&search=kinnosuke+++++hon');
expect(guilds.length).to.equal(1);
expect(guilds[0].description).to.have.string('Kinnosuke');
});
});
describe('public guilds pagination', () => {
it('req.query.paginate must be a boolean string', async () => {
await expect(user.get('/groups?paginate=aString&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('req.query.paginate can only be true when req.query.type includes publicGuilds', async () => {
await expect(user.get('/groups?paginate=true&type=notPublicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: apiError('guildsOnlyPaginate'),
});
});
it('req.query.page can\'t be negative', async () => {
await expect(user.get('/groups?paginate=true&page=-1&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns 30 guilds per page ordered by number of members', async () => {
await user.update({ balance: 9000 });
const delay = () => new Promise(resolve => setTimeout(resolve, 40));
const promises = [];
for (let i = 0; i < 60; i += 1) {
promises.push(generateGroup(user, {
name: `public guild ${i} - is member`,
type: 'guild',
privacy: 'public',
}));
await delay(); // eslint-disable-line no-await-in-loop
}
const groups = await Promise.all(promises);
// update group number 32 and not the first to make sure sorting works
await groups[32].update({ name: 'guild with most members', memberCount: 199 });
await groups[33].update({ name: 'guild with less members', memberCount: -100 });
const page0 = await expect(user.get('/groups?type=publicGuilds&paginate=true'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
expect(page0[0].name).to.equal('guild with most members');
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
const page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
// 1 created now, 4 by other tests, -1 for no more tavern.
.to.eventually.have.a.lengthOf(1 + 4 - 1);
expect(page2[3].name).to.equal('guild with less members');
}).timeout(10000);
});
it('makes sure that the tavern doesn\'t show up when guilds is passed as a query', async () => {
const guilds = await user.get('/groups?type=guilds');
expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined;
});
it('makes sure that the tavern doesn\'t show up when publicGuilds is passed as a query', async () => {
const guilds = await user.get('/groups?type=publicGuilds');
expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined;
});
it('returns all the user\'s guilds when guilds passed in as query', async () => {
await expect(user.get('/groups?type=guilds'))
.to.eventually.have.a
.lengthOf(NUMBER_OF_USERS_PRIVATE_GUILDS);
.lengthOf(NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER + NUMBER_OF_USERS_PRIVATE_GUILDS);
});
it('returns all private guilds user is a part of when privateGuilds passed in as query', async () => {
@@ -109,21 +272,21 @@ describe('GET /groups', () => {
});
it('returns a list of groups user has access to', async () => {
await expect(user.get('/groups?type=privateGuilds,party'))
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW);
await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern'))
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW - 1); // -1 for no Tavern.
});
describe('filters', () => {
it('returns private guilds filtered by category', async () => {
const guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id);
it('returns a list of groups user has access to', async () => {
const group = await generateGroup(user, {
name: 'c++ coders',
type: 'guild',
privacy: 'public',
});
it('filters private guilds by size', async () => {
const guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
// search for 'c++ coders'
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=0&search=c%2B%2B+coders'))
.to.eventually.have.lengthOf(1)
.and.to.have.nested.property('[0]')
.and.to.have.property('_id', group._id);
});
});

View File

@@ -3,7 +3,6 @@ import {
generateUser,
generateGroup,
translate as t,
createAndPopulateGroup,
} from '../../../../helpers/api-integration/v3';
describe('GET /groups/:groupId/invites', () => {
@@ -72,16 +71,15 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns only first 30 invites by default (req.query.limit not specified)', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 31,
upgradeToGroupPlan: true,
});
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const invitesToGenerate = [];
for (let i = 0; i < 31; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate);
await leader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id) });
const res = await leader.get(`/groups/${group._id}/invites`);
expect(res.length).to.equal(30);
@@ -92,16 +90,8 @@ describe('GET /groups/:groupId/invites', () => {
}).timeout(10000);
it('returns an error if req.query.limit is over 60', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 1,
upgradeToGroupPlan: true,
});
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
await expect(leader.get(`/groups/${group._id}/invites?limit=61`)).to.eventually.be.rejected.and.eql({
code: 400,
@@ -111,16 +101,8 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns an error if req.query.limit is under 1', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 1,
upgradeToGroupPlan: true,
});
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
await expect(leader.get(`/groups/${group._id}/invites?limit=-1`)).to.eventually.be.rejected.and.eql({
code: 400,
@@ -130,16 +112,8 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns an error if req.query.limit is not an integer', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 1,
upgradeToGroupPlan: true,
});
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
await expect(leader.get(`/groups/${group._id}/invites?limit=1.3`)).to.eventually.be.rejected.and.eql({
code: 400,
@@ -149,16 +123,15 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns up to 60 invites when req.query.limit is specified', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 31,
upgradeToGroupPlan: true,
});
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const invitesToGenerate = [];
for (let i = 0; i < 31; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate);
await leader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id) });
let res = await leader.get(`/groups/${group._id}/invites?limit=14`);
expect(res.length).to.equal(14);
@@ -176,20 +149,17 @@ describe('GET /groups/:groupId/invites', () => {
}).timeout(30000);
it('supports using req.query.lastId to get more invites', async function test () {
let group; let invitees;
this.timeout(30000); // @TODO: times out after 8 seconds
({ group, groupLeader: user, invitees } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 32,
upgradeToGroupPlan: true,
}));
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const expectedIds = invitees.map(generatedInvite => generatedInvite._id);
const invitesToGenerate = [];
for (let i = 0; i < 32; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate); // Group has 32 invites
const expectedIds = generatedInvites.map(generatedInvite => generatedInvite._id);
await user.post(`/groups/${group._id}/invite`, { uuids: expectedIds });
const res = await user.get(`/groups/${group._id}/invites`);
expect(res.length).to.equal(30);

View File

@@ -1,6 +1,5 @@
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
generateGroup,
translate as t,
@@ -76,15 +75,7 @@ describe('GET /groups/:groupId/members', () => {
});
it('req.query.includeAllPublicFields === true works with guilds', async () => {
let group;
({ group, groupLeader: user } = await createAndPopulateGroup({
type: 'guild',
privacy: 'private',
name: generateUUID(),
upgradeToGroupPlan: true,
members: 1,
}));
const group = await generateGroup(user, { type: 'guild', name: generateUUID() });
const [memberRes] = await user.get(`/groups/${group._id}/members?includeAllPublicFields=true`);
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
@@ -215,20 +206,20 @@ describe('GET /groups/:groupId/members', () => {
it('supports using req.query.lastId to get more members', async function test () {
this.timeout(30000); // @TODO: times out after 8 seconds
const { group, groupLeader: leader, members: generatedUsers } = await createAndPopulateGroup({
type: 'guild',
privacy: 'private',
name: generateUUID(),
upgradeToGroupPlan: true,
leaderDetails: { balance: 4 },
members: 57,
});
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const usersToGenerate = [];
for (let i = 0; i < 57; i += 1) {
usersToGenerate.push(generateUser({ guilds: [group._id] }));
}
// Group has 59 members (1 is the leader)
const generatedUsers = await Promise.all(usersToGenerate);
const expectedIds = [leader._id].concat(generatedUsers.map(generatedUser => generatedUser._id));
const res = await leader.get(`/groups/${group._id}/members`);
const res = await user.get(`/groups/${group._id}/members`);
expect(res.length).to.equal(30);
const res2 = await leader.get(`/groups/${group._id}/members?lastId=${res[res.length - 1]._id}`);
const res2 = await user.get(`/groups/${group._id}/members?lastId=${res[res.length - 1]._id}`);
expect(res2.length).to.equal(28);
const resIds = res.concat(res2).map(member => member._id);

View File

@@ -11,6 +11,7 @@ import {
describe('GET /groups/:id', () => {
const typesOfGroups = {};
typesOfGroups['public guild'] = { type: 'guild', privacy: 'public' };
typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' };
typesOfGroups.party = { type: 'party', privacy: 'private' };
@@ -23,11 +24,10 @@ describe('GET /groups/:id', () => {
const groupData = await createAndPopulateGroup({
members: 30,
groupDetails,
upgradeToGroupPlan: groupDetails.type === 'guild',
});
leader = groupData.groupLeader;
[member] = groupData.members;
member = groupData.members[0]; // eslint-disable-line prefer-destructuring
createdGroup = groupData.group;
});
@@ -49,6 +49,34 @@ describe('GET /groups/:id', () => {
});
});
context('Non-member of a public guild', () => {
let nonMember; let
createdGroup;
before(async () => {
const groupData = await createAndPopulateGroup({
members: 1,
groupDetails: {
name: 'test guild',
type: 'guild',
privacy: 'public',
},
});
createdGroup = groupData.group;
nonMember = await generateUser();
});
it('returns the group object for a non-member', async () => {
const group = await nonMember.get(`/groups/${createdGroup._id}`);
expect(group._id).to.eql(createdGroup._id);
expect(group.name).to.eql(createdGroup.name);
expect(group.type).to.eql(createdGroup.type);
expect(group.privacy).to.eql(createdGroup.privacy);
});
});
context('Non-member of a private guild', () => {
let nonMember; let
createdGroup;
@@ -61,7 +89,6 @@ describe('GET /groups/:id', () => {
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
createdGroup = groupData.group;
@@ -191,7 +218,7 @@ describe('GET /groups/:id', () => {
});
context('Flagged messages', () => {
let group; let members;
let group;
const chat1 = {
id: 'chat1',
@@ -241,7 +268,7 @@ describe('GET /groups/:id', () => {
groupDetails: {
name: 'test guild',
type: 'guild',
privacy: 'private',
privacy: 'public',
chat: [
chat1,
chat2,
@@ -250,11 +277,9 @@ describe('GET /groups/:id', () => {
chat5,
],
},
members: 1,
upgradeToGroupPlan: true,
});
({ group, members } = groupData);
group = groupData.group;
await group.addChat([chat1, chat2, chat3, chat4, chat5]);
});
@@ -262,8 +287,8 @@ describe('GET /groups/:id', () => {
context('non-admin', () => {
let nonAdmin;
beforeEach(() => {
[nonAdmin] = members;
beforeEach(async () => {
nonAdmin = await generateUser();
});
it('does not include messages with a flag count of 2 or greater', async () => {
@@ -289,8 +314,9 @@ describe('GET /groups/:id', () => {
let admin;
beforeEach(async () => {
[admin] = members;
await admin.update({ permissions: { moderator: true } });
admin = await generateUser({
'permissions.moderator': true,
});
});
it('includes all messages', async () => {

View File

@@ -2,6 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('POST /group', () => {
@@ -34,8 +35,8 @@ describe('POST /group', () => {
it('sets the group leader to the user who created the group', async () => {
const group = await user.post('/groups', {
name: 'Test Party',
type: 'party',
name: 'Test Public Guild',
type: 'guild',
});
expect(group.leader).to.eql({
@@ -50,7 +51,7 @@ describe('POST /group', () => {
const name = 'Test Group';
const group = await user.post('/groups', {
name,
type: 'party',
type: 'guild',
});
const updatedGroup = await user.get(`/groups/${group._id}`);
@@ -63,7 +64,7 @@ describe('POST /group', () => {
const summary = 'Test Summary';
const group = await user.post('/groups', {
name,
type: 'party',
type: 'guild',
summary,
});
@@ -77,7 +78,7 @@ describe('POST /group', () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(user.post('/groups', {
name,
type: 'party',
type: 'guild',
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
@@ -87,6 +88,157 @@ describe('POST /group', () => {
});
});
context('Guilds', () => {
it('returns an error when a user with insufficient funds attempts to create a guild', async () => {
await user.update({ balance: 0 });
await expect(
user.post('/groups', {
name: 'Test Public Guild',
type: 'guild',
}),
).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageInsufficientGems'),
});
});
it('adds guild to user\'s list of guilds', async () => {
const guild = await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
const updatedUser = await user.get('/user');
expect(updatedUser.guilds).to.include(guild._id);
});
it('awards the Joined Guild achievement', async () => {
await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
const updatedUser = await user.get('/user');
expect(updatedUser.achievements.joinedGuild).to.eql(true);
});
context('public guild', () => {
it('creates a group', async () => {
const groupName = 'Test Public Guild';
const groupType = 'guild';
const groupPrivacy = 'public';
const publicGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(publicGuild._id).to.exist;
expect(publicGuild.name).to.equal(groupName);
expect(publicGuild.type).to.equal(groupType);
expect(publicGuild.memberCount).to.equal(1);
expect(publicGuild.privacy).to.equal(groupPrivacy);
expect(publicGuild.leader).to.eql({
_id: user._id,
profile: {
name: user.profile.name,
},
});
});
it('returns an error when a user with no chat privileges attempts to create a public guild', async () => {
await user.update({ 'flags.chatRevoked': true });
await expect(
user.post('/groups', {
name: 'Test Public Guild',
type: 'guild',
privacy: 'public',
}),
).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
});
context('private guild', () => {
const groupName = 'Test Private Guild';
const groupType = 'guild';
const groupPrivacy = 'private';
it('creates a group', async () => {
const privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild._id).to.exist;
expect(privateGuild.name).to.equal(groupName);
expect(privateGuild.type).to.equal(groupType);
expect(privateGuild.memberCount).to.equal(1);
expect(privateGuild.privacy).to.equal(groupPrivacy);
expect(privateGuild.leader).to.eql({
_id: user._id,
profile: {
name: user.profile.name,
},
});
});
it('creates a private guild when the user has no chat privileges', async () => {
await user.update({ 'flags.chatRevoked': true });
const privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild._id).to.exist;
});
it('deducts gems from user and adds them to guild bank', async () => {
const privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild.balance).to.eql(1);
const updatedUser = await user.get('/user');
expect(updatedUser.balance).to.eql(user.balance - 1);
});
it('does not deduct the gems from user when guild creation fails', async () => {
const stub = sinon.stub(Group.prototype, 'save').rejects();
const promise = user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
await expect(promise).to.eventually.be.rejected;
const updatedUser = await user.get('/user');
expect(updatedUser.balance).to.eql(user.balance);
stub.restore();
});
});
});
context('Parties', () => {
const partyName = 'Test Party';
const partyType = 'party';

View File

@@ -18,24 +18,81 @@ describe('POST /group/:groupId/join', () => {
});
});
context('Joining a private guild', () => {
let user;
let invitedUser;
let guild;
let invitees;
context('Joining a public guild', () => {
let user; let joiningUser; let
publicGuild;
beforeEach(async () => {
({ group: guild, groupLeader: user, invitees } = await createAndPopulateGroup({
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
},
});
publicGuild = group;
user = groupLeader;
joiningUser = await generateUser();
});
it('allows non-invited users to join public guilds', async () => {
const res = await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.property('guilds').to.include(publicGuild._id);
expect(res.leader._id).to.eql(user._id);
expect(res.leader.profile.name).to.eql(user.profile.name);
});
it('returns an error if user was already a member', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.post(`/groups/${publicGuild._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('youAreAlreadyInGroup'),
});
});
it('promotes joining member in a public empty guild to leader', async () => {
await user.post(`/groups/${publicGuild._id}/leave`);
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.nested.property('leader._id', joiningUser._id);
});
it('increments memberCount when joining guilds', async () => {
const oldMemberCount = publicGuild.memberCount;
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
});
it('awards Joined Guild achievement', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.nested.property('achievements.joinedGuild', true);
});
});
context('Joining a private guild', () => {
let user; let invitedUser; let
guild;
beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'private',
},
invites: 1,
upgradeToGroupPlan: true,
}));
});
[invitedUser] = invitees;
guild = group;
user = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
it('returns error when user is not invited to private guild', async () => {
@@ -125,7 +182,7 @@ describe('POST /group/:groupId/join', () => {
party = group;
user = groupLeader;
[invitedUser] = invitees;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
it('returns error when user is not invited to party', async () => {

View File

@@ -5,6 +5,7 @@ import {
generateChallenge,
checkExistence,
createAndPopulateGroup,
sleep,
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -13,187 +14,253 @@ import payments from '../../../../../website/server/libs/payments/payments';
import calculateSubscriptionTerminationDate from '../../../../../website/server/libs/payments/calculateSubscriptionTerminationDate';
describe('POST /groups/:groupId/leave', () => {
let groupToLeave;
let leader;
let member;
let members;
let memberCount;
const typesOfGroups = {
'public guild': { type: 'guild', privacy: 'public' },
'private guild': { type: 'guild', privacy: 'private' },
party: { type: 'party', privacy: 'private' },
};
context('Leaving a Group Plan', () => {
beforeEach(async () => {
({ group: groupToLeave, groupLeader: leader, members } = await createAndPopulateGroup({
type: 'guild',
privacy: 'private',
members: 1,
upgradeToGroupPlan: true,
}));
[member] = members;
memberCount = groupToLeave.memberCount;
await leader.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('prevents non members from leaving', async () => {
const user = await generateUser();
await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('lets user leave', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userThatLeftGroup = await member.get('/user');
expect(userThatLeftGroup.guilds).to.be.empty;
expect(userThatLeftGroup.party._id).to.not.exist;
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
});
it('removes new messages for that group from user', async () => {
await leader.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
await member.sync();
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist;
expect(member.newMessages[groupToLeave._id]).to.not.be.empty;
await member.post(`/groups/${groupToLeave._id}/leave`);
await member.sync();
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist;
expect(member.newMessages[groupToLeave._id]).to.be.undefined;
});
context('with challenges', () => {
let challenge;
each(typesOfGroups, (groupDetails, groupType) => {
context(`Leaving a ${groupType}`, () => {
let groupToLeave;
let leader;
let member;
let memberCount;
beforeEach(async () => {
challenge = await generateChallenge(leader, groupToLeave);
await member.post(`/challenges/${challenge._id}/join`);
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails,
members: 1,
});
await leader.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
groupToLeave = group;
leader = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
memberCount = group.memberCount;
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('prevents non members from leaving', async () => {
const user = await generateUser();
await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('removes all challenge tasks when keep parameter is set to remove', async () => {
await member.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`);
const userWithoutChallengeTasks = await member.get('/user');
expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id);
expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty;
});
it('keeps all challenge tasks when keep parameter is not set', async () => {
it(`lets user leave a ${groupType}`, async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user');
const userThatLeftGroup = await member.get('/user');
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
expect(userThatLeftGroup.guilds).to.be.empty;
expect(userThatLeftGroup.party._id).to.not.exist;
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
});
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' });
it(`sets a new group leader when leader leaves a ${groupType}`, async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
expect(groupToLeave.leader).to.equal(member._id);
});
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
it('removes new messages for that group from user', async () => {
await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
const userWithChallengeTasks = await member.get('/user');
await sleep(0.5);
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
await leader.sync();
expect(leader.notifications.find(n => 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 => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist;
expect(leader.newMessages[groupToLeave._id]).to.be.undefined;
});
context('with challenges', () => {
let challenge;
beforeEach(async () => {
challenge = await generateChallenge(leader, groupToLeave);
await leader.post(`/challenges/${challenge._id}/join`);
await leader.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
});
await sleep(0.5);
});
it('removes all challenge tasks when keep parameter is set to remove', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`);
const userWithoutChallengeTasks = await leader.get('/user');
expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id);
expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty;
});
it('keeps all challenge tasks when keep parameter is not set', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await leader.get('/user');
// @TODO find elegant way to assert against the task existing
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
});
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' });
const userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
});
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
});
});
it('prevents quest leader from leaving a groupToLeave');
it('prevents a user from leaving during an active quest');
});
});
context('Leaving a Party', () => {
let invitees;
let invitedUser;
context('Leaving a group as the last member', () => {
context('private guild', () => {
let privateGuild;
let leader;
let invitedUser;
beforeEach(async () => {
({
group: groupToLeave,
groupLeader: leader,
members,
invitees,
} = await createAndPopulateGroup({
type: 'party',
privacy: 'private',
members: 1,
invites: 1,
}));
beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Private Guild',
type: 'guild',
},
invites: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
[member] = members;
[invitedUser] = invitees;
memberCount = groupToLeave.memberCount;
await leader.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
privateGuild = group;
leader = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
it('prevents non members from leaving', async () => {
const user = await generateUser();
await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
await leader.post(`/groups/${group._id}/chat`, { message: 'Some message' });
});
it('removes a group when the last member leaves', async () => {
await leader.post(`/groups/${privateGuild._id}/leave`);
await expect(checkExistence('groups', privateGuild._id)).to.eventually.equal(false);
});
it('removes invitations when the last member leaves', async () => {
await leader.post(`/groups/${privateGuild._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.guilds).to.be.empty;
});
});
it('lets user leave', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
context('public guild', () => {
let publicGuild;
let leader;
let invitedUser;
const userThatLeftGroup = await member.get('/user');
beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Public Guild',
type: 'guild',
privacy: 'public',
},
invites: 1,
});
expect(userThatLeftGroup.guilds).to.be.empty;
expect(userThatLeftGroup.party._id).to.not.exist;
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
publicGuild = group;
leader = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
it('keeps the group when the last member leaves', async () => {
await leader.post(`/groups/${publicGuild._id}/leave`);
await expect(checkExistence('groups', publicGuild._id)).to.eventually.equal(true);
});
it('keeps the invitations when the last member leaves a public guild', async () => {
await leader.post(`/groups/${publicGuild._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.guilds).to.not.be.empty;
});
it('deletes non existent guild from user when user tries to leave', async () => {
const nonExistentGuildId = generateUUID();
const userWithNonExistentGuild = await generateUser({ guilds: [nonExistentGuildId] });
expect(userWithNonExistentGuild.guilds).to.contain(nonExistentGuildId);
await expect(userWithNonExistentGuild.post(`/groups/${nonExistentGuildId}/leave`))
.to.eventually.be.rejected;
await userWithNonExistentGuild.sync();
expect(userWithNonExistentGuild.guilds).to.not.contain(nonExistentGuildId);
});
});
it('sets a new group leader when leader leaves', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
context('party', () => {
let party;
let leader;
let invitedUser;
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
expect(groupToLeave.leader).to.equal(member._id);
});
beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Party',
type: 'party',
},
invites: 1,
});
it('removes new messages for that group from user', async () => {
await leader.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
await member.sync();
party = group;
leader = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist;
expect(member.newMessages[groupToLeave._id]).to.not.be.empty;
it('removes a group when the last member leaves a party', async () => {
await leader.post(`/groups/${party._id}/leave`);
await member.post(`/groups/${groupToLeave._id}/leave`);
await member.sync();
await expect(checkExistence('party', party._id)).to.eventually.equal(false);
});
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist;
expect(member.newMessages[groupToLeave._id]).to.be.undefined;
});
it('removes invitations when the last member leaves a party', async () => {
await leader.post(`/groups/${party._id}/leave`);
it('removes a party when the last member leaves', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
await expect(checkExistence('party', groupToLeave._id)).to.eventually.equal(false);
});
it('removes invitations when the last member leaves a party', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.parties[0]).to.be.undefined;
expect(userWithoutInvitation.invitations.parties[0]).to.be.undefined;
});
});
it('deletes non existent party from user when user tries to leave', async () => {
@@ -208,71 +275,23 @@ describe('POST /groups/:groupId/leave', () => {
expect(userWithNonExistentParty.party).to.eql({});
});
context('with challenges', () => {
let challenge;
beforeEach(async () => {
challenge = await generateChallenge(leader, groupToLeave);
await member.post(`/challenges/${challenge._id}/join`);
await leader.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
});
});
it('removes all challenge tasks when keep parameter is set to remove', async () => {
await member.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`);
const userWithoutChallengeTasks = await member.get('/user');
expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id);
expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty;
});
it('keeps all challenge tasks when keep parameter is not set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
});
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' });
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
});
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
});
});
});
const typesOfGroups = {
'private guild': { type: 'guild', privacy: 'private' },
party: { type: 'party', privacy: 'private' },
};
each(typesOfGroups, (groupDetails, groupType) => {
context(`Leaving a group plan when the group is a ${groupType}`, () => {
if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans
let groupWithPlan;
let leader;
let member;
beforeEach(async () => {
({ group: groupWithPlan, groupLeader: leader, members } = await createAndPopulateGroup({
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails,
members: 1,
upgradeToGroupPlan: true,
}));
[member] = members;
});
leader = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
groupWithPlan = group;
const userWithFreePlan = await User.findById(leader._id).exec();
// Create subscription
@@ -302,21 +321,45 @@ describe('POST /groups/:groupId/leave', () => {
await member.sync();
expect(member.purchased.plan.dateTerminated).to.exist;
});
it('preserves the free subscription when leaving a any other group without a plan', async () => {
// Joining a guild without a group plan
const { group: groupWithNoPlan } = await createAndPopulateGroup({
groupDetails: {
name: 'Group Without Plan',
type: 'guild',
privacy: 'public',
},
});
await member.post(`/groups/${groupWithNoPlan._id}/join`);
await member.sync();
expect(member.purchased.plan.planId).to.equal('group_plan_auto');
expect(member.purchased.plan.dateTerminated).to.not.exist;
// Leaving the guild without a group plan
await member.post(`/groups/${groupWithNoPlan._id}/leave`);
await member.sync();
expect(member.purchased.plan.dateTerminated).to.not.exist;
});
});
});
each(typesOfGroups, (groupDetails, groupType) => {
context(`Leaving a group with extraMonths left plan when the group is a ${groupType}`, () => {
if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans
const extraMonths = 12;
let groupWithPlan;
let member;
beforeEach(async () => {
({ group: groupWithPlan, members } = await createAndPopulateGroup({
const { group, members } = await createAndPopulateGroup({
groupDetails,
members: 1,
upgradeToGroupPlan: true,
}));
});
[member] = members;
groupWithPlan = group;
await member.update({
'purchased.plan.extraMonths': extraMonths,
});

View File

@@ -5,6 +5,43 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('POST /group/:groupId/reject-invite', () => {
context('Rejecting a public guild invite', () => {
let publicGuild; let
invitedUser;
beforeEach(async () => {
const { group, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
},
invites: 1,
});
publicGuild = group;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
it('returns error when user is not invited', async () => {
const userWithoutInvite = await generateUser();
await expect(userWithoutInvite.post(`/groups/${publicGuild._id}/reject-invite`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupRequiresInvite'),
});
});
it('clears invitation from user', async () => {
await invitedUser.post(`/groups/${publicGuild._id}/reject-invite`);
await expect(invitedUser.get('/user'))
.to.eventually.have.nested.property('invitations.guilds')
.to.not.include({ id: publicGuild._id });
});
});
context('Rejecting a private guild invite', () => {
let invitedUser; let
guild;
@@ -17,7 +54,6 @@ describe('POST /group/:groupId/reject-invite', () => {
privacy: 'private',
},
invites: 1,
upgradeToGroupPlan: true,
});
guild = group;

View File

@@ -25,7 +25,6 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
},
invites: 1,
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
@@ -130,11 +129,9 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
it('sends email to removed user', async () => {
await leader.post(`/groups/${guild._id}/removeMember/${member._id}`);
expect(email.sendTxn).to.be.calledTwice;
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.eql(member._id);
expect(email.sendTxn.args[0][1]).to.eql('kicked-from-guild');
expect(email.sendTxn.args[1][0]._id).to.eql(member._id);
expect(email.sendTxn.args[1][1]).to.eql('group-member-removed');
});
});

View File

@@ -3,6 +3,7 @@ import nconf from 'nconf';
import {
createAndPopulateGroup,
generateUser,
generateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -13,13 +14,13 @@ const MAX_EMAIL_INVITES_BY_USER = 200;
describe('Post /groups/:groupId/invite', () => {
let inviter;
let group;
const groupName = 'Test Party';
const groupName = 'Test Public Guild';
beforeEach(async () => {
inviter = await generateUser({ balance: 4 });
group = await inviter.post('/groups', {
name: groupName,
type: 'party',
type: 'guild',
});
});
@@ -64,44 +65,45 @@ describe('Post /groups/:groupId/invite', () => {
it('invites a user to a group by username', async () => {
const userToInvite = await generateUser();
const response = await inviter.post(`/groups/${group._id}/invite`, {
await expect(inviter.post(`/groups/${group._id}/invite`, {
usernames: [userToInvite.auth.local.lowerCaseUsername],
});
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
})).to.eventually.deep.equal([{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
}]);
await expect(userToInvite.get('/user'))
.to.eventually.have.nested.property('invitations.parties[0].id', group._id);
.to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
});
it('invites multiple users to a group by uuid', async () => {
const userToInvite = await generateUser();
const userToInvite2 = await generateUser();
const response = await (inviter.post(`/groups/${group._id}/invite`, {
await expect(inviter.post(`/groups/${group._id}/invite`, {
usernames: [
userToInvite.auth.local.lowerCaseUsername,
userToInvite2.auth.local.lowerCaseUsername,
],
}));
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[1]._id).to.be.a('String');
expect(response[1].id).to.eql(group._id);
expect(response[1].name).to.eql(groupName);
expect(response[1].inviter).to.eql(inviter._id);
})).to.eventually.deep.equal([
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
]);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
});
});
@@ -212,42 +214,42 @@ describe('Post /groups/:groupId/invite', () => {
it('invites a user to a group by uuid', async () => {
const userToInvite = await generateUser();
const response = await inviter.post(`/groups/${group._id}/invite`, {
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
});
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
})).to.eventually.deep.equal([{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
}]);
await expect(userToInvite.get('/user'))
.to.eventually.have.nested.property('invitations.parties[0].id', group._id);
.to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
});
it('invites multiple users to a group by uuid', async () => {
const userToInvite = await generateUser();
const userToInvite2 = await generateUser();
const response = await inviter.post(`/groups/${group._id}/invite`, {
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id, userToInvite2._id],
});
})).to.eventually.deep.equal([
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
]);
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[1]._id).to.be.a('String');
expect(response[1].id).to.eql(group._id);
expect(response[1].name).to.eql(groupName);
expect(response[1].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
});
it('returns an error when inviting multiple users and a user is not found', async () => {
@@ -336,8 +338,12 @@ describe('Post /groups/:groupId/invite', () => {
invitesSent: MAX_EMAIL_INVITES_BY_USER,
balance: 4,
});
const tmpGroup = await inviterWithMax.post('/groups', {
name: groupName,
type: 'guild',
});
await expect(inviterWithMax.post(`/groups/${group._id}/invite`, {
await expect(inviterWithMax.post(`/groups/${tmpGroup._id}/invite`, {
emails: [testInvite],
inviter: 'inviter name',
}))
@@ -413,15 +419,15 @@ describe('Post /groups/:groupId/invite', () => {
});
const invitedUser = await newUser.get('/user');
expect(invitedUser.invitations.parties[0].id).to.equal(group._id);
expect(invitedUser.invitations.guilds[0].id).to.equal(group._id);
expect(invite).to.exist;
});
it('invites user to group with cancelled plan', async () => {
let cancelledPlanGroup;
({ group: cancelledPlanGroup, groupLeader: inviter } = await createAndPopulateGroup({
upgradeToGroupPlan: true,
}));
it('invites marks invite with cancelled plan', async () => {
const cancelledPlanGroup = await generateGroup(inviter, {
type: 'guild',
name: generateUUID(),
});
await cancelledPlanGroup.createCancelledSubscription();
const newUser = await generateUser();
@@ -431,13 +437,13 @@ describe('Post /groups/:groupId/invite', () => {
});
const invitedUser = await newUser.get('/user');
expect(invitedUser.invitations.parties[0].id).to.equal(cancelledPlanGroup._id);
expect(invitedUser.invitations.parties[0].cancelledPlan).to.be.true;
expect(invitedUser.invitations.guilds[0].id).to.equal(cancelledPlanGroup._id);
expect(invitedUser.invitations.guilds[0].cancelledPlan).to.be.true;
expect(invite).to.exist;
});
});
describe('party invites', () => {
describe('guild invites', () => {
it('returns an error when inviter has no chat privileges', async () => {
const inviterMuted = await inviter.update({ 'flags.chatRevoked': true });
const userToInvite = await generateUser();
@@ -451,7 +457,7 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('returns an error when invited user has a pending invitation to the party', async () => {
it('returns an error when invited user is already invited to the group', async () => {
const userToInvite = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
@@ -459,6 +465,96 @@ describe('Post /groups/:groupId/invite', () => {
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userAlreadyInvitedToGroup', { userId: userToInvite._id, username: userToInvite.profile.name }),
});
});
it('returns an error when invited user is already in the group', async () => {
const userToInvite = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
});
await userToInvite.post(`/groups/${group._id}/join`);
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userAlreadyInGroup', { userId: userToInvite._id, username: userToInvite.profile.name }),
});
});
it('allows 30+ members in a guild', async () => {
const invitesToGenerate = [];
// Generate 30 users to invite (30 + leader = 31 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await inviter.post(`/groups/${group._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
}).timeout(10000);
// @TODO: Add this after we are able to mock the group plan route
xit('returns an error when a non-leader invites to a group plan', async () => {
const userToInvite = await generateUser();
const nonGroupLeader = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [nonGroupLeader._id],
});
await nonGroupLeader.post(`/groups/${group._id}/join`);
await expect(nonGroupLeader.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanInviteToGroupPlan'),
});
});
});
describe('party invites', () => {
let party;
beforeEach(async () => {
party = await inviter.post('/groups', {
name: 'Test Party',
type: 'party',
});
});
it('returns an error when inviter has no chat privileges', async () => {
const inviterMuted = await inviter.update({ 'flags.chatRevoked': true });
const userToInvite = await generateUser();
await expect(inviterMuted.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('returns an error when invited user has a pending invitation to the party', async () => {
const userToInvite = await generateUser();
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
await expect(inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
@@ -470,13 +566,13 @@ describe('Post /groups/:groupId/invite', () => {
it('returns an error when invited user is already in a party of more than 1 member', async () => {
const userToInvite = await generateUser();
const userToInvite2 = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id, userToInvite2._id],
});
await userToInvite.post(`/groups/${group._id}/join`);
await userToInvite2.post(`/groups/${group._id}/join`);
await userToInvite.post(`/groups/${party._id}/join`);
await userToInvite2.post(`/groups/${party._id}/join`);
await expect(inviter.post(`/groups/${group._id}/invite`, {
await expect(inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
@@ -500,7 +596,7 @@ describe('Post /groups/:groupId/invite', () => {
});
// Invite to first party
await inviter.post(`/groups/${group._id}/invite`, {
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
@@ -513,27 +609,28 @@ describe('Post /groups/:groupId/invite', () => {
const invitedUser = await userToInvite.get('/user');
expect(invitedUser.invitations.parties.length).to.equal(2);
expect(invitedUser.invitations.parties[0].id).to.equal(group._id);
expect(invitedUser.invitations.parties[0].id).to.equal(party._id);
expect(invitedUser.invitations.parties[1].id).to.equal(party2._id);
});
it('allows inviting a user if party id is not associated with a real party', async () => {
it('allow inviting a user if party id is not associated with a real party', async () => {
const userToInvite = await generateUser({
party: { _id: generateUUID() },
});
await inviter.post(`/groups/${group._id}/invite`, {
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(group._id);
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id);
});
});
describe('party size limits', () => {
let party;
let partyLeader;
beforeEach(async () => {
({ group, groupLeader: partyLeader } = await createAndPopulateGroup({
group = await createAndPopulateGroup({
groupDetails: {
name: 'Test Party',
type: 'party',
@@ -541,7 +638,9 @@ describe('Post /groups/:groupId/invite', () => {
},
// Generate party with 20 members
members: PARTY_LIMIT_MEMBERS - 10,
}));
});
party = group.group;
partyLeader = group.groupLeader;
});
it('allows 30 members in a party', async () => {
@@ -552,7 +651,7 @@ describe('Post /groups/:groupId/invite', () => {
}
const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await partyLeader.post(`/groups/${group._id}/invite`, {
expect(await partyLeader.post(`/groups/${party._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
}).timeout(10000);
@@ -565,7 +664,7 @@ describe('Post /groups/:groupId/invite', () => {
}
const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
await expect(partyLeader.post(`/groups/${group._id}/invite`, {
await expect(partyLeader.post(`/groups/${party._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
}))
.to.eventually.be.rejected.and.eql({

View File

@@ -17,10 +17,9 @@ describe('POST /group/:groupId/add-manager', () => {
groupDetails: {
name: groupName,
type: groupType,
privacy: 'private',
privacy: 'public',
},
members: 1,
upgradeToGroupPlan: true,
});
groupToUpdate = group;

View File

@@ -23,11 +23,10 @@ describe('PUT /group', () => {
groupDetails: {
name: groupName,
type: groupType,
privacy: 'private',
privacy: 'public',
categories: groupCategories,
},
members: 1,
upgradeToGroupPlan: true,
});
adminUser = await generateUser({ 'permissions.moderator': true });
groupToUpdate = group;
@@ -107,28 +106,14 @@ describe('PUT /group', () => {
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('does not allow a leader to change leader of active group plan', async () => {
await expect(leader.put(`/groups/${groupToUpdate._id}`, {
it('allows a leader to change leaders', async () => {
const updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
leader: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotChangeLeaderWithActiveGroupPlan'),
});
});
it('allows a leader of a party to change leaders', async () => {
const { group: party, groupLeader: partyLeader, members } = await createAndPopulateGroup({
members: 1,
});
const updatedGroup = await partyLeader.put(`/groups/${party._id}`, {
name: groupUpdatedName,
leader: members[0]._id,
});
expect(updatedGroup.leader._id).to.eql(members[0]._id);
expect(updatedGroup.leader.profile.name).to.eql(members[0].profile.name);
expect(updatedGroup.leader._id).to.eql(nonLeader._id);
expect(updatedGroup.leader.profile.name).to.eql(nonLeader.profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
@@ -137,16 +122,15 @@ describe('PUT /group', () => {
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
upgradeToGroupPlan: true,
});
const updateGroupDetails = {
id: group._id,
name: 'public guild',
type: 'guild',
privacy: 'private',
privacy: 'public',
bannedWordsAllowed: true,
};
@@ -166,11 +150,9 @@ describe('PUT /group', () => {
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
upgradeToGroupPlan: true,
});
await groupLeader.update({ permissions: {} });
const updateGroupDetails = {
id: group._id,

View File

@@ -1,6 +1,6 @@
import {
createAndPopulateGroup,
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
@@ -50,21 +50,22 @@ describe('payments : amazon #subscribeCancel', () => {
});
it('cancels a group subscription', async () => {
({ group, groupLeader: user } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
leaderDetails: {
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
},
upgradeToGroupPlan: true,
}));
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.get(`${endpoint}&groupId=${group._id}`);

View File

@@ -70,8 +70,8 @@ describe('payments - amazon - #subscribe', () => {
group = await generateGroup(user, {
name: 'test group',
type: 'party',
privacy: 'private',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),

View File

@@ -1,7 +1,7 @@
import {
generateUser,
generateGroup,
translate as t,
createAndPopulateGroup,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
@@ -48,21 +48,22 @@ describe('payments - stripe - #subscribeCancel', () => {
});
it('cancels a group subscription', async () => {
({ group, groupLeader: user } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
leaderDetails: {
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
},
upgradeToGroupPlan: true,
}));
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.get(`${endpoint}&groupId=${group._id}`);

View File

@@ -53,7 +53,6 @@ describe('POST /groups/:groupId/quests/accept', () => {
it('does not accept quest for a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/accept`))

View File

@@ -43,7 +43,6 @@ describe('POST /groups/:groupId/quests/force-start', () => {
it('does not force start quest for a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/force-start`))

View File

@@ -51,13 +51,14 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
});
it('does not issue invites for Guilds', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
const { group } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'public' },
members: 1,
upgradeToGroupPlan: true,
});
await expect(groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
const alternateGroup = group;
await expect(leader.post(`/groups/${alternateGroup._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('guildQuestsNotSupported'),

View File

@@ -52,7 +52,6 @@ describe('POST /groups/:groupId/quests/abort', () => {
it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/abort`))

View File

@@ -52,7 +52,6 @@ describe('POST /groups/:groupId/quests/cancel', () => {
it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/cancel`))

View File

@@ -51,7 +51,6 @@ describe('POST /groups/:groupId/quests/leave', () => {
it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/leave`))

View File

@@ -53,7 +53,6 @@ describe('POST /groups/:groupId/quests/reject', () => {
it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/reject`))

View File

@@ -1,5 +1,6 @@
import {
createAndPopulateGroup,
generateUser,
generateGroup,
} from '../../../../../helpers/api-integration/v3';
describe('POST group-tasks/:taskId/move/to/:position', () => {
@@ -7,12 +8,8 @@ describe('POST group-tasks/:taskId/move/to/:position', () => {
guild;
beforeEach(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
guild = group;
user = groupLeader;
user = await generateUser({ balance: 1 });
guild = await generateGroup(user, { type: 'guild' }, { 'purchased.plan.customerId': 'group-unlimited' });
});
it('can move task to new position', async () => {

View File

@@ -1,4 +1,5 @@
import {
find,
each,
map,
} from 'lodash';
@@ -197,6 +198,95 @@ describe('DELETE /user', () => {
await expect(checkExistence('party', party._id)).to.eventually.eql(false);
});
});
context('last member of a private guild', () => {
let privateGuild;
beforeEach(async () => {
privateGuild = await generateGroup(user, {
type: 'guild',
privacy: 'private',
});
});
it('deletes guild when user is the only member', async () => {
await user.del('/user', {
password,
});
await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false);
});
});
context('groups user is leader of', () => {
let guild; let oldLeader; let
newLeader;
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 1,
});
guild = group;
newLeader = members[0]; // eslint-disable-line prefer-destructuring
oldLeader = groupLeader;
});
it('chooses new group leader for any group user was the leader of', async () => {
await oldLeader.del('/user', {
password,
});
const updatedGuild = await newLeader.get(`/groups/${guild._id}`);
expect(updatedGuild.leader).to.exist;
expect(updatedGuild.leader._id).to.not.eql(oldLeader._id);
});
});
context('groups user is a part of', () => {
let group1; let group2; let userToDelete; let
otherUser;
beforeEach(async () => {
userToDelete = await generateUser({ balance: 10 });
group1 = await generateGroup(userToDelete, {
type: 'guild',
privacy: 'public',
});
const { group, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 3,
});
group2 = group;
otherUser = members[0]; // eslint-disable-line prefer-destructuring
await userToDelete.post(`/groups/${group2._id}/join`);
});
it('removes user from all groups user was a part of', async () => {
await userToDelete.del('/user', {
password,
});
const updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`);
const updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`);
const userInGroup = find(updatedGroup2Members, member => member._id === userToDelete._id);
expect(updatedGroup1Members).to.be.empty;
expect(updatedGroup2Members).to.not.be.empty;
expect(userInGroup).to.not.exist;
});
});
});
context('user with Google auth', async () => {

View File

@@ -51,7 +51,6 @@ describe('POST /user/purchase/:type/:key', () => {
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
await group.update({
'leaderOnly.getGems': true,
@@ -78,7 +77,6 @@ describe('POST /user/purchase/:type/:key', () => {
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
});
await group.update({
'leaderOnly.getGems': true,

View File

@@ -714,6 +714,31 @@ describe('POST /user/auth/local/register', () => {
expect(user.invitations.party).to.eql({});
});
it('adds a user to a guild on an invite of type other than party', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
const invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
const user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.guilds[0]).to.eql({
id: group._id,
name: group.name,
inviter: groupLeader._id,
});
});
});
context('successful login via api', () => {

View File

@@ -202,86 +202,18 @@ describe('POST /user/class/cast/:spellId', () => {
await group.groupLeader.post('/user/class/cast/mpheal');
promises = [];
promises.push(group.groupLeader.sync());
promises.push(group.members[0].sync());
promises.push(group.members[1].sync());
promises.push(group.members[2].sync());
promises.push(group.members[3].sync());
await Promise.all(promises);
expect(group.groupLeader.stats.mp).to.be.equal(170); // spell caster
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
expect(group.members[1].stats.mp).to.equal(0); // wizard
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
});
const spellList = [
{
className: 'warrior',
spells: [['smash', 'task'], ['defensiveStance'], ['valorousPresence'], ['intimidate']],
},
{
className: 'wizard',
spells: [['fireball', 'task'], ['mpheal'], ['earth'], ['frost']],
},
{
className: 'healer',
spells: [['heal'], ['brightness'], ['protectAura'], ['healAll']],
},
{
className: 'rogue',
spells: [['pickPocket', 'task'], ['backStab', 'task'], ['toolsOfTrade'], ['stealth']],
},
];
spellList.forEach(async habitClass => {
describe(`For a ${habitClass.className}`, async () => {
habitClass.spells.forEach(async spell => {
describe(`Using ${spell[0]}`, async () => {
it('Deducts MP from spell caster', async () => {
const { groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 3,
});
await groupLeader.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await groupLeader.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await groupLeader.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await groupLeader.post(`/user/class/cast/${spell[0]}`);
}
await groupLeader.sync();
expect(groupLeader.stats.mp).to.be.lessThan(200);
});
it('works without a party', async () => {
await user.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await user.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await user.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await user.post(`/user/class/cast/${spell[0]}`);
}
await user.sync();
expect(user.stats.mp).to.be.lessThan(200);
});
});
});
});
});
it('cast bulk', async () => {
let { group, groupLeader } = await createAndPopulateGroup({ // eslint-disable-line prefer-const
groupDetails: { type: 'party', privacy: 'private' },

View File

@@ -665,7 +665,6 @@ describe('POST /user/auth/local/register', () => {
it('adds a user to a guild on an invite of type other than party', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
const invite = encrypt(JSON.stringify({

View File

@@ -127,9 +127,6 @@ export async function createAndPopulateGroup (settings = {}) {
const upgradeToGroupPlan = settings.upgradeToGroupPlan || false;
const { groupDetails } = settings;
const leaderDetails = settings.leaderDetails || { balance: 10 };
if (upgradeToGroupPlan) {
leaderDetails.permissions = { fullAccess: true };
}
const groupLeader = await generateUser(leaderDetails);
const group = await generateGroup(groupLeader, groupDetails);

View File

@@ -120,9 +120,6 @@ export async function createAndPopulateGroup (settings = {}) {
const upgradeToGroupPlan = settings.upgradeToGroupPlan || false;
const { groupDetails } = settings;
const leaderDetails = settings.leaderDetails || { balance: 10 };
if (upgradeToGroupPlan) {
leaderDetails.permissions = { fullAccess: true };
}
const groupLeader = await generateUser(leaderDetails);
const group = await generateGroup(groupLeader, groupDetails);

View File

@@ -1842,9 +1842,9 @@
}
},
"@babel/plugin-proposal-optional-chaining": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
"integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==",
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz",
"integrity": "sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==",
"requires": {
"@babel/helper-plugin-utils": "^7.20.2",
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
@@ -1870,9 +1870,9 @@
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="
},
"@babel/types": {
"version": "7.21.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz",
"integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==",
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz",
"integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==",
"requires": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
@@ -13318,11 +13318,34 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"optional": true
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"loader-utils": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -13354,6 +13377,38 @@
"ansi-regex": "^5.0.1"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -16852,9 +16907,9 @@
}
},
"core-js": {
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz",
"integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ=="
"version": "3.27.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.2.tgz",
"integrity": "sha512-9ashVQskuh5AZEZ1JdQWp1GqSoC1e1G87MzRqg2gIfVAQ7Qn9K+uFj8EcniUFA4P2NLZfV+TOlX1SzoKfo+s7w=="
},
"core-js-compat": {
"version": "3.11.0",
@@ -17903,9 +17958,9 @@
}
},
"dompurify": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz",
"integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ=="
},
"domutils": {
"version": "1.7.0",
@@ -21022,11 +21077,6 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
},
"immutable": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg=="
},
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -21387,9 +21437,9 @@
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
},
"intro.js": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-7.0.1.tgz",
"integrity": "sha512-1oqz6aOz9cGQ3CrtVYhCSo6AkjnXUn302kcIWLaZ3TI4kKssRXDwDSz4VRoGcfC1jN+WfaSJXRBrITz+QVEBzg=="
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-6.0.0.tgz",
"integrity": "sha512-ZUiR6BoLSvPSlLG0boewnWVgji1fE1gBvP/pyw5pgCKXEDQz1mMeUxarggClPNs71UTq364LwSk9zxz17A9gaQ=="
},
"invariant": {
"version": "2.2.4",
@@ -21969,9 +22019,9 @@
}
},
"jquery": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz",
"integrity": "sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg=="
},
"js-message": {
"version": "1.0.5",
@@ -27317,19 +27367,17 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sass": {
"version": "1.63.4",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.4.tgz",
"integrity": "sha512-Sx/+weUmK+oiIlI+9sdD0wZHsqpbgQg8wSwSnGBjwb5GwqFhYNwwnI+UWZtLjKvKyFlKkatRK235qQ3mokyPoQ==",
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.34.0.tgz",
"integrity": "sha512-rHEN0BscqjUYuomUEaqq3BMgsXqQfkcMVR7UhscsAVub0/spUrZGBMxQXFS2kfiDsPLZw5yuU9iJEFNC2x38Qw==",
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
"chokidar": ">=3.0.0 <4.0.0"
},
"dependencies": {
"anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -27349,18 +27397,18 @@
}
},
"chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"requires": {
"anymatch": "~3.1.2",
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
"readdirp": "~3.5.0"
}
},
"fill-range": {
@@ -27399,9 +27447,9 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"requires": {
"picomatch": "^2.2.1"
}
@@ -27753,9 +27801,9 @@
}
},
"smartbanner.js": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/smartbanner.js/-/smartbanner.js-1.19.2.tgz",
"integrity": "sha512-hwcGNp5Hza5PJHTmqP6H8q0XBYhloIQyJgdzv0ldz3HQSeEuKB2riVraQXdKuquE6ZU/0M0yubno53xJ/ZiQQg=="
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/smartbanner.js/-/smartbanner.js-1.19.1.tgz",
"integrity": "sha512-x3alFTlk6pLuqrm9PrYQv1E+86CrEIgPf/KJ+nP5342BmOWstbdR8OwD3TPmM56zHQm4MEr/eoqbEcfTKdvdKw=="
},
"snapdragon": {
"version": "0.8.2",
@@ -28127,9 +28175,9 @@
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"stopword": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/stopword/-/stopword-2.0.8.tgz",
"integrity": "sha512-btlEC2vEuhCuvshz99hSGsY8GzaP5qzDPQm56j6rR/R38p8xdsOXgU5a6tIgvU/4hcCta1Vlo/2FVXA9m0f8XA=="
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/stopword/-/stopword-2.0.7.tgz",
"integrity": "sha512-s+uLKAxrproCLrq0Wcd3JAIjlJLx6l80b2Rzt0u8+ra5SzGkHnNG8PS3DfGmYk2TrKePDVLL4SdKYwKpgSLc+w=="
},
"store2": {
"version": "2.10.0",
@@ -30223,9 +30271,9 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"uuid-browser": {
"version": "3.1.0",
@@ -30581,85 +30629,6 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"optional": true
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"optional": true
},
"loader-utils": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"vue-mugen-scroll": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/vue-mugen-scroll/-/vue-mugen-scroll-0.2.6.tgz",

View File

@@ -32,8 +32,8 @@
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"chai": "^4.3.7",
"core-js": "^3.31.0",
"dompurify": "^3.0.3",
"core-js": "^3.27.2",
"dompurify": "^2.4.3",
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.3.0",
@@ -41,20 +41,20 @@
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
"intro.js": "^7.0.1",
"jquery": "^3.7.0",
"intro.js": "^6.0.0",
"jquery": "^3.6.3",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nconf": "^0.12.0",
"sass": "^1.63.4",
"sass": "^1.34.0",
"sass-loader": "^8.0.2",
"smartbanner.js": "^1.19.2",
"stopword": "^2.0.8",
"smartbanner.js": "^1.19.1",
"stopword": "^2.0.7",
"svg-inline-loader": "^0.8.2",
"svg-url-loader": "^7.1.1",
"svgo": "^1.3.2",
"svgo-loader": "^2.2.1",
"uuid": "^9.0.0",
"uuid": "^8.3.2",
"validator": "^13.9.0",
"vue": "^2.7.10",
"vue-cli-plugin-storybook": "2.1.0",
@@ -66,6 +66,6 @@
"webpack": "^4.46.0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0"
"@babel/plugin-proposal-optional-chaining": "^7.20.7"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 B

View File

@@ -41,7 +41,6 @@
<router-view v-if="!isUserLoggedIn || isStaticPage" />
<template v-else>
<template v-if="isUserLoaded">
<chat-banner />
<damage-paused-banner />
<gems-promo-banner />
<gift-promo-banner />
@@ -160,7 +159,6 @@ import { loadProgressBar } from 'axios-progress-bar';
import birthdayModal from '@/components/news/birthdayModal';
import AppMenu from './components/header/menu';
import AppHeader from './components/header/index';
import ChatBanner from './components/header/banners/chatBanner';
import DamagePausedBanner from './components/header/banners/damagePaused';
import GemsPromoBanner from './components/header/banners/gemsPromo';
import GiftPromoBanner from './components/header/banners/giftPromo';
@@ -200,7 +198,6 @@ export default {
AppHeader,
AppFooter,
birthdayModal,
ChatBanner,
DamagePausedBanner,
GemsPromoBanner,
GiftPromoBanner,

View File

@@ -94,12 +94,6 @@
height: 90px;
}
.back_special_heroicAureole {
width: 114px;
height: 90px;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
}
.head_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
}
@@ -198,6 +192,14 @@
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
}
/* FIXME figure out how to handle customize menu!!
.customize-menu .f_head_0 {
width: 60px;
height: 60px;
background-position: -1917px -9px;
}
*/
[class*="Mount_Head_"],
[class*="Mount_Body_"] {
margin-top:18px; /* Sprite accommodates 105x123 box */

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,26 @@
<template>
<div class="row">
<div class="col-6 text-center mx-auto mb-5">
<div class="col-12 text-center">
<!-- @TODO i18n. How to setup the strings with the router-link inside?-->
<img
:class="retiredChatPage ? 'mt-5' : 'image-404'"
class="not-found-img"
src="~@/assets/images/404.png"
>
<div v-if="retiredChatPage">
<h1>
{{ $t('tavernDiscontinued') }}
</h1>
<p>{{ $t('tavernDiscontinuedDetail') }}</p>
<p v-html="$t('tavernDiscontinuedLinks')"></p>
</div>
<div v-else>
<h1>
Sometimes even the bravest adventurer gets lost.
</h1>
<p class="mb-0">
Looks like this link is broken or the page may have moved, sorry!
</p>
<p>
Head back to the
<router-link to="/">
Homepage
</router-link>or
<router-link :to="contactUsLink">
Contact Us
</router-link>about the issue.
</p>
</div>
<h1 class="not-found">
Sometimes even the bravest adventurer gets lost.
</h1>
<h2 class="not-found">
Looks like this link is broken or the page may have moved, sorry!
</h2>
<h2 class="not-found">
Head back to the
<router-link to="/">
Homepage
</router-link>or
<router-link :to="contactUsLink">
Contact Us
</router-link>about the issue.
</h2>
</div>
</div>
</template>
@@ -46,9 +37,6 @@ export default {
}
return { name: 'contact' };
},
retiredChatPage () {
return this.$route.fullPath.indexOf('/groups') !== -1;
},
},
};
</script>
@@ -56,20 +44,28 @@ export default {
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
h1, .static-wrapper h1 {
color: $purple-200;
.col-12 {
margin-bottom: 120px;
}
.not-found-img {
margin-top: 152px;
margin-bottom: 42px;
}
h1.not-found {
line-height: 1.33;
margin-top: 3rem;
margin-bottom: 1rem;
color: $purple-200;
margin-bottom: 8px;
font-weight: normal;
margin-top: 0px;
}
p {
font-size: 16px;
line-height: 1.75;
h2.not-found {
line-height: 1.4;
font-weight: normal;
color: $gray-200;
margin-bottom: 0px;
margin-top: 0px;
}
.image-404 {
margin-top: 104px;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<b-modal
id="testing"
:title="$t('guildReminderTitle')"
size="lg"
:hide-footer="true"
>
<div class="modal-body text-center">
<br>
<div class="scene_guilds"></div>
<br>
<h4>{{ $t('guildReminderText1') }}</h4>
</div>
<div class="modal-footer">
<div class="container-fluid">
<div class="row">
<div class="col-6 text-center">
<button
class="btn btn-secondary"
@click="close()"
>
{{ $t('guildReminderDismiss') }}
</button>
</div>
<div
class="col-6 text-center"
@click="close()"
>
<div
class="btn btn-primary"
@click="takeMethere()"
>
{{ $t('guildReminderCTA') }}
</div>
</div>
</div>
</div>
</div>
</b-modal>
</template>
<style scoped>
.scene_guilds {
margin: 0 auto;
}
</style>
<script>
export default {
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'testing');
},
takeMethere () {
this.$router.push('/groups/discovery');
this.close();
},
},
};
</script>

View File

@@ -0,0 +1,58 @@
<template>
<b-modal
id="testingletiant"
:title="$t('guildReminderTitle')"
size="lg"
:hide-footer="true"
>
<div class="modal-content"></div>
<div class="modal-body text-center">
<br>
<div class="scene_guilds"></div>
<br>
<h4>{{ $t('guildReminderText2') }}</h4>
</div>
<div class="modal-footer">
<div class="container-fluid">
<div class="row">
<div class="col-6 text-center">
<button
class="btn btn-secondary"
@click="close()"
>
{{ $t('guildReminderDismiss') }}
</button>
</div>
<div class="col-6 text-center">
<div
class="btn btn-primary"
@click="takeMethere()"
>
{{ $t('guildReminderCTA') }}
</div>
</div>
</div>
</div>
</div>
</b-modal>
</template>
<style scoped>
.scene_guilds {
margin: 0 auto;
}
</style>
<script>
export default {
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'testingletiant');
},
takeMethere () {
this.$router.push('/groups/discovery');
this.close();
},
},
};
</script>

View File

@@ -17,18 +17,10 @@
Payment schedule ("basic-earned" is monthly):
<strong>{{ hero.purchased.plan.planId }}</strong>
</div>
<div v-if="hero.purchased.plan.planId == 'group_plan_auto'">
Group plan ID:
<strong>{{ hero.purchased.plan.owner }}</strong>
</div>
<div v-if="hero.purchased.plan.dateCreated">
Creation date:
<strong>{{ dateFormat(hero.purchased.plan.dateCreated) }}</strong>
</div>
<div v-if="hero.purchased.plan.dateCurrentTypeCreated">
Start date for current subscription type:
<strong>{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}</strong>
</div>
<div>
Termination date:
<strong
@@ -54,16 +46,9 @@
Perk offset months:
<strong>{{ hero.purchased.plan.consecutive.offset }}</strong>
</div>
<div class="form-inline">
<div>
Perk month count:
<input
v-model="hero.purchased.plan.perkMonthCount"
class="form-control"
type="number"
min="0"
max="2"
step="1"
>
<strong>{{ hero.purchased.plan.perkMonthCount }}</strong>
</div>
<div>
Next Mystic Hourglass:

View File

@@ -8,15 +8,23 @@
slot="modal-header"
class="bug-report-modal-header"
>
<h2>
{{ question ? $t('askQuestion') : $t('reportBug') }}
<h2 v-once>
{{ $t('reportBug') }}
</h2>
<div class="report-bug-header-describe">
{{ question ? $t('askQuestionHeaderDescribe') : $t('reportBugHeaderDescribe') }}
<div
v-once
class="report-bug-header-describe"
>
{{ $t('reportBugHeaderDescribe') }}
</div>
<div class="dialog-close">
<close-icon
:purple="true"
@click="close()"
/>
</div>
<close-x
@close="close()"
/>
</div>
<div>
<form
@@ -32,8 +40,11 @@
>
{{ $t('email') }}
</label>
<div class="mb-2 description-label">
{{ question ? $t('questionEmailText') : $t('reportEmailText') }}
<div
v-once
class="mb-2 description-label"
>
{{ $t('reportEmailText') }}
</div>
<input
id="emailInput"
@@ -53,18 +64,21 @@
</div>
</div>
<label>
{{ question ? $t('question') : $t('reportDescription') }}
<label v-once>
{{ $t('reportDescription') }}
</label>
<div class="mb-2 description-label">
{{ question ? $t('questionDescriptionText') : $t('reportDescriptionText') }}
<div
v-once
class="mb-2 description-label"
>
{{ $t('reportDescriptionText') }}
</div>
<textarea
v-model="message"
class="form-control"
rows="5"
:required="true"
:placeholder="question ? $t('questionPlaceholder') : $t('reportDescriptionPlaceholder')"
:placeholder="$t('reportDescriptionPlaceholder')"
:class="{'input-invalid': messageInvalid && this.message.length === 0}"
>
@@ -75,7 +89,7 @@
type="submit"
:disabled="!message || !emailValid"
>
{{ question ? $t('submitQuestion') : $t('submitBugReport') }}
{{ $t('submitBugReport') }}
</button>
</form>
</div>
@@ -127,7 +141,7 @@ h2 {
.bug-report-modal-header {
color: $white;
width: 100%;
padding: 1.5rem 3rem 1.5rem 1.5rem;
padding: 2rem 3rem 1.5rem 1.5rem;
background-image: linear-gradient(288deg, #{$purple-200}, #{$purple-300});
}
@@ -168,13 +182,13 @@ label {
<script>
import axios from 'axios';
import isEmail from 'validator/lib/isEmail';
import closeX from '@/components/ui/closeX';
import closeIcon from '@/components/shared/closeIcon';
import { mapState } from '@/libs/store';
import { MODALS } from '@/libs/consts';
export default {
components: {
closeX,
closeIcon,
},
data () {
return {
@@ -197,7 +211,6 @@ export default {
await axios.post('/api/v4/bug-report', {
message: this.message,
email: this.email,
question: this.question,
});
this.message = '';
@@ -220,9 +233,6 @@ export default {
if (this.email.length <= 3) return false;
return !this.emailValid;
},
question () {
return this.$store.state.bugReportOptions.question;
},
},
mounted () {
const { user } = this;

View File

@@ -50,6 +50,7 @@ import notificationsMixin from '@/mixins/notifications';
import Task from '@/components/tasks/task';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import { TAVERN_ID } from '@/../../common/script/constants';
const baseUrl = 'https://habitica.com';
@@ -88,7 +89,9 @@ export default {
createTask: 'tasks:create',
}),
groupPath () {
if (this.groupType === 'party') {
if (this.groupId === TAVERN_ID) {
return `${baseUrl}/groups/tavern`;
} if (this.groupType === 'party') {
return `${baseUrl}/party`;
}
return `${baseUrl}/groups/guild/${this.groupId}`;

View File

@@ -183,8 +183,10 @@
<div
v-for="bg in backgroundShopSets[0].items"
:key="bg.key"
:id="bg.key"
class="col-2"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="unlock('background.' + bg.key)"
>
<div
@@ -193,13 +195,6 @@
>
<div class="small-rectangle"></div>
</div>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
</div>
<div
@@ -216,21 +211,16 @@
<div
v-for="bg in backgroundShopSets[2].items"
:key="bg.key"
:id="bg.key"
class="col-4 text-center customize-option background-button"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="unlock('background.' + bg.key)"
>
<div
class="background"
:class="`background_${bg.key}`"
></div>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
</div>
</div>
@@ -246,8 +236,10 @@
<div
v-for="bg in backgroundShopSets[1].items"
:key="bg.key"
:id="bg.key"
class="col-4 text-center customize-option background-button"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="!user.purchased.background[bg.key]
? backgroundSelected(bg) : unlock('background.' + bg.key)"
>
@@ -278,13 +270,6 @@
:pinned="isBackgroundPinned(bg)"
/>
</span>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
</div>
</div>
@@ -317,8 +302,10 @@
<div
v-for="bg in set.items"
:key="bg.key"
:id="bg.key"
class="col-4 text-center customize-option background-button"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="!user.purchased.background[bg.key]
? backgroundSelected(bg) : unlock('background.' + bg.key)"
>
@@ -349,13 +336,6 @@
:pinned="isBackgroundPinned(bg)"
/>
</span>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
<div
v-if="!ownsSet('background', set.items) && set.identifier !== 'incentiveBackgrounds'"
@@ -378,21 +358,16 @@
<div
v-for="(bg) in ownedBackgrounds"
:key="bg.key"
:id="bg.key"
class="col-4 text-center customize-option background-button"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="unlock('background.' + bg.key)"
>
<div
class="background"
:class="[`background_${bg.key}`, backgroundLockedStatus(bg.key)]"
></div>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
</div>
</div>

View File

@@ -188,7 +188,6 @@
padding: 8px;
border-radius: 4px;
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
background-color: $white;
&:first-of-type {
margin-top: 24px;

View File

@@ -258,22 +258,13 @@
:key="hero._id"
>
<td>
<div
<user-link
v-if="hasPermission(hero, 'userSupport')"
class="width-content"
>
<user-link
:id="hero._id"
:user="hero"
/>
<b-popover
:target="hero._id"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('gamemaster')"
/>
</div>
:user="hero"
:popover="$t('gamemaster')"
popover-trigger="mouseenter"
popover-placement="right"
/>
<user-link
v-else
:user="hero"
@@ -311,10 +302,6 @@
h4.expand-toggle::after {
margin-left: 5px;
}
.width-content {
width: fit-content;
}
</style>
<script>

View File

@@ -6,10 +6,13 @@
:style="{height}"
>
<slot name="content"></slot>
<close-x
<div
v-if="canClose"
@close="close()"
/>
class="close-icon svg-icon icon-12"
@click="close()"
v-html="icons.close"
></div>
</div>
</template>
@@ -27,24 +30,32 @@ body.modal-open .habitica-top-banner {
padding-left: 1.5rem;
padding-right: 1.625rem;
z-index: 1300;
}
.modal-close {
position: unset;
.close-icon.svg-icon {
position: relative;
top: 0;
right: 0;
opacity: 0.48;
& ::v-deep svg path {
stroke: $white !important;
}
&:hover {
opacity: 0.75;
}
}
</style>
<script>
import closeX from '@/components/ui/closeX';
import closeIcon from '@/assets/svg/close.svg';
import {
clearBannerSetting, hideBanner, isBannerHidden, updateBannerHeight,
} from '@/libs/banner.func';
import { EVENTS } from '@/libs/events';
export default {
components: {
closeX,
},
props: {
bannerId: {
type: String,
@@ -71,6 +82,9 @@ export default {
},
data () {
return {
icons: Object.freeze({
close: closeIcon,
}),
hidden: false,
};
},
@@ -105,6 +119,8 @@ export default {
close () {
hideBanner(this.bannerId);
this.hidden = true;
this.$root.$emit(EVENTS.BANNER_HIDDEN, this.bannerId);
},
},
};

View File

@@ -1,64 +0,0 @@
<template>
<base-banner
banner-id="chat-warning"
banner-class="chat-banner"
class="chat-banner"
height="3rem"
v-if="showChatWarning"
:class="{faq: faqPage}"
>
<div
slot="content"
class="w-100 text-center"
v-html="$t('chatSunsetWarning')"
>
</div>
</base-banner>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
.chat-banner {
width: 100%;
min-height: 48px;
padding: 8px;
color: $orange-1;
background-color: $orange-100;
line-height: 1.71;
a {
color: $orange-1;
text-decoration: underline;
&:hover {
color: $orange-1;
}
}
&.faq {
position: fixed;
top: 3.5rem;
}
}
</style>
<script>
import BaseBanner from './base';
export default {
components: {
BaseBanner,
},
computed: {
faqPage () {
return (this.$route.fullPath.indexOf('/faq')) !== -1;
},
showChatWarning () {
return false;
},
},
};
</script>

View File

@@ -194,6 +194,48 @@
>
{{ $t('party') }}
</b-nav-item>
<li
class="topbar-item droppable"
:class="{
'active': $route.path.startsWith('/groups')}"
>
<div
class="chevron rotate"
@click="dropdownMobile($event)"
>
<div
v-once
class="chevron-icon-down"
v-html="icons.chevronDown"
></div>
</div>
<router-link
class="nav-link"
:to="{name: 'tavern'}"
>
{{ $t('guilds') }}
</router-link>
<div class="topbar-dropdown">
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'tavern'}"
>
{{ $t('tavern') }}
</router-link>
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'myGuilds'}"
>
{{ $t('myGuilds') }}
</router-link>
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'guildsDiscovery'}"
>
{{ $t('guildsDiscovery') }}
</router-link>
</div>
</li>
<li
class="topbar-item droppable"
:class="{
@@ -312,18 +354,22 @@
>
{{ $t('reportBug') }}
</a>
<a
<router-link
class="topbar-dropdown-item dropdown-item"
target="_blank"
@click.prevent="openBugReportModal(true)"
to="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a"
>
{{ $t('askQuestion') }}
</a>
</router-link>
<a
class="topbar-dropdown-item dropdown-item"
href="https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link"
target="_blank"
>{{ $t('requestFeature') }}</a>
<a
class="topbar-dropdown-item dropdown-item"
href="https://habitica.fandom.com/wiki/Contributing_to_Habitica"
target="_blank"
>{{ $t('contributing') }}</a>
<a
class="topbar-dropdown-item dropdown-item"
href="https://habitica.fandom.com/wiki/Habitica_Wiki"
@@ -615,7 +661,6 @@ body.modal-open #habitica-menu {
&:hover {
background: $purple-300;
text-decoration: none;
&:last-child {
border-bottom-right-radius: 5px;
@@ -785,11 +830,9 @@ export default {
async mounted () {
await this.getUserGroupPlans();
await this.getUserParty();
if (document.getElementById('menu_collapse')) {
Array.from(document.getElementById('menu_collapse').getElementsByTagName('a')).forEach(link => {
link.addEventListener('click', this.closeMenu);
});
}
Array.from(document.getElementById('menu_collapse').getElementsByTagName('a')).forEach(link => {
link.addEventListener('click', this.closeMenu);
});
Array.from(document.getElementsByClassName('topbar-item')).forEach(link => {
link.addEventListener('mouseenter', this.dropdownDesktop);
link.addEventListener('mouseleave', this.dropdownDesktop);

View File

@@ -44,13 +44,13 @@ export default {
if (!this.notification || !this.notification.data) {
return;
}
if (this.notification.data.destination.indexOf('backgrounds') !== -1) {
if (this.notification.data.destination === 'backgrounds') {
this.$store.state.avatarEditorOptions.editingUser = true;
this.$store.state.avatarEditorOptions.startingPage = 'backgrounds';
this.$store.state.avatarEditorOptions.subpage = '2023';
this.$root.$emit('bv::show::modal', 'avatar-modal');
} else {
this.$router.push(this.notification.data.destination || '/inventory/items');
this.$router.push({ name: this.notification.data.destination || 'items' });
}
},
},

View File

@@ -11,6 +11,8 @@
<low-health />
<level-up />
<choose-class />
<testing />
<testingletiant />
<rebirth-enabled />
<contributor />
<won-challenge />
@@ -125,6 +127,8 @@ import chooseClass from './achievements/chooseClass';
import armoireEmpty from './achievements/armoireEmpty';
import questCompleted from './achievements/questCompleted';
import questInvitation from './achievements/questInvitation';
import testing from './achievements/testing';
import testingletiant from './achievements/testingletiant';
import rebirthEnabled from './achievements/rebirthEnabled';
import contributor from './achievements/contributor';
import invitedFriend from './achievements/invitedFriend';
@@ -265,6 +269,8 @@ export default {
armoireEmpty,
questCompleted,
questInvitation,
testing,
testingletiant,
rebirthEnabled,
contributor,
loginIncentives,
@@ -294,6 +300,7 @@ export default {
// general notifications
'CRON',
'FIRST_DROPS',
'GUILD_PROMPT',
'LOGIN_INCENTIVE',
'NEW_CONTRIBUTOR_LEVEL',
'ONBOARDING_COMPLETE',
@@ -698,6 +705,14 @@ export default {
this.$root.$emit('bv::show::modal', 'first-drops');
}
break;
case 'GUILD_PROMPT':
// @TODO: I'm pretty sure we can find better names for these
if (notification.data.textletiant === -1) {
this.$root.$emit('bv::show::modal', 'testing');
} else {
this.$root.$emit('bv::show::modal', 'testingletiant');
}
break;
case 'REBIRTH_ENABLED':
this.$root.$emit('bv::show::modal', 'rebirth-enabled');
break;

View File

@@ -126,7 +126,7 @@
<!-- the word "total" -->
<div class="buy-gem-total">
{{ $t('sendTotal') }}
{{ $t('sendGiftTotal') }}
</div>
<!-- the actual dollar amount -->

View File

@@ -128,10 +128,7 @@
<hr>
</div>
<div>
<div
class="checkbox"
id="preferenceAdvancedCollapsed"
>
<div class="checkbox">
<label>
<input
v-model="user.preferences.advancedCollapsed"
@@ -139,22 +136,33 @@
class="mr-2"
@change="set('advancedCollapsed')"
>
<span class="hint">
{{ $t('startAdvCollapsed') }}
</span>
<b-popover
target="preferenceAdvancedCollapsed"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('startAdvCollapsedPop')"
/>
<span
class="hint"
popover-trigger="mouseenter"
popover-placement="right"
:popover="$t('startAdvCollapsedPop')"
>{{ $t('startAdvCollapsed') }}</span>
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="user.preferences.dailyDueDefaultView"
type="checkbox"
class="mr-2"
@change="set('dailyDueDefaultView')"
>
<span
class="hint"
popover-trigger="mouseenter"
popover-placement="right"
:popover="$t('dailyDueDefaultViewPop')"
>{{ $t('dailyDueDefaultView') }}</span>
</label>
</div>
<div
v-if="party.memberCount === 1"
class="checkbox"
id="preferenceDisplayInviteAtOneMember"
>
<label>
<input
@@ -163,9 +171,12 @@
class="mr-2"
@change="set('displayInviteToPartyWhenPartyIs1')"
>
<span class="hint">
{{ $t('displayInviteToPartyWhenPartyIs1') }}
</span>
<span
class="hint"
popover-trigger="mouseenter"
popover-placement="right"
:popover="$t('displayInviteToPartyWhenPartyIs1')"
>{{ $t('displayInviteToPartyWhenPartyIs1') }}</span>
</label>
</div>
<div class="checkbox">
@@ -206,47 +217,32 @@
</div>
<hr>
<button
id="buttonShowBailey"
class="btn btn-primary mr-2 mb-2"
popover-trigger="mouseenter"
popover-placement="right"
:popover="$t('showBaileyPop')"
@click="showBailey()"
>
{{ $t('showBailey') }}
<b-popover
target="buttonShowBailey"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('showBaileyPop')"
/>
</button>
<button
id="buttonFCV"
class="btn btn-primary mr-2 mb-2"
popover-trigger="mouseenter"
popover-placement="right"
:popover="$t('fixValPop')"
@click="openRestoreModal()"
>
{{ $t('fixVal') }}
<b-popover
target="buttonFCV"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('fixValPop')"
/>
</button>
<button
v-if="user.preferences.disableClasses == true"
id="buttonEnableClasses"
class="btn btn-primary mb-2"
popover-trigger="mouseenter"
popover-placement="right"
:popover="$t('enableClassPop')"
@click="changeClassForUser(false)"
>
{{ $t('enableClass') }}
<b-popover
target="buttonEnableClasses"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('enableClassPop')"
/>
</button>
<hr>
<day-start-adjustment />
@@ -536,10 +532,6 @@
input {
color: $gray-50;
}
.checkbox {
width: fit-content;
}
.usersettings h5 {
margin-top: 1em;
}

View File

@@ -1,93 +0,0 @@
<template>
<div class="item-cost">
<span
class="cost"
:class="getPriceClass()"
>
<span
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons[getPriceClass()]"
>
</span>
<span
:class="getPriceClass()"
>{{ item.value }}</span>
</span>
</div>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/mixins.scss';
.item-cost {
padding-bottom: 16px;
}
.cost {
height: 40px;
font-size: 1.25rem;
font-weight: bold;
line-height: 1.4;
vertical-align: middle;
&.gems {
color: $gems-color;
border-radius: 20px;
padding: 8px 20px 8px 20px;
margin-top: 16px;
margin-bottom: 16px;
background-color: rgba(36, 204, 143, 0.15);
}
&.gold {
color: $gold-color;
border-radius: 20px;
padding: 8px 20px 8px 20px;
margin-top: 16px;
margin-bottom: 16px;
background-color: rgba(255, 190, 93, 0.15);
}
&.hourglasses {
color: $hourglass-color;
border-radius: 20px;
padding: 8px 20px 8px 20px;
margin-top: 16px;
margin-bottom: 16px;
background-color: rgba(41, 149, 205, 0.15);
}
}
</style>
<script>
import svgClose from '@/assets/svg/close.svg';
import svgGold from '@/assets/svg/gold.svg';
import svgGem from '@/assets/svg/gem.svg';
export default {
data () {
return {
icons: Object.freeze({
close: svgClose,
gold: svgGold,
gems: svgGem,
}),
selectedAmountToBuy: 1,
selectedAmount: 1,
};
},
methods: {
getPriceClass () {
if (this.priceType && this.icons[this.priceType]) {
return this.priceType;
} if (this.item.currency && this.icons[this.item.currency]) {
return this.item.currency;
}
return 'gold';
},
},
};
</script>

View File

@@ -1,137 +0,0 @@
<template>
<div class="d-flex flex-row align-items-center justify-content-center number-increment">
<!-- buy modal -->
<div
class="gray-circle"
@click="quantity <= 0
? quantity = 0
: quantity--"
>
<div
class="icon-negative"
v-html="icons.svgNegative"
></div>
</div>
<div class="input-group">
<div class="align-items-center">
</div>
<input
v-model="quantity"
class="form-control alignment"
step="1"
type="number"
>
</div>
<div
class="gray-circle"
@click="quantity++"
>
<div
class="icon-positive"
v-html="icons.svgPositive"
></div>
</div>
</div>
</template>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
.number-increment {
padding-bottom: 0px;
}
.alignment {
text-align: center;
}
.input-group {
width: 94px;
height: 32px;
width: 48px;
margin: 0px 16px 0px 16px;
padding: 0;
border-radius: 2px;
border: solid 1px $gray-400;
background-color: $white;
}
.gray-circle {
border-radius: 100%;
border: solid 2px $gray-300;
width: 32px;
height: 32px;
cursor: pointer;
&:hover {
border-color: $purple-300;
}
}
.gray-circle:hover{
.icon-positive, .icon-negative {
& ::v-deep svg path {
fill: $purple-300;
}
}
}
.icon-positive, .icon-negative {
width: 10px;
height: 10px;
margin: 4px auto;
& ::v-deep svg path {
fill: $gray-300;
}
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
</style>
<script>
// icons
import svgGem from '@/assets/svg/gem.svg';
import svgGold from '@/assets/svg/gold.svg';
import svgPositive from '@/assets/svg/positive.svg';
import svgNegative from '@/assets/svg/negative.svg';
export default {
data () {
return {
icons: Object.freeze({
svgGem,
svgGold,
svgPositive,
svgNegative,
}),
item: { },
quantity: 1,
};
},
computed: {
},
watch: {
quantity () {
this.$emit('updateQuantity', this.quantity);
},
},
methods: {
setDefaults () {
this.input = 1;
},
},
};
</script>

View File

@@ -22,11 +22,10 @@
@import '~@/assets/scss/colors.scss';
span {
font-size: 0.75rem;
font-weight: normal;
font-size: 12px;
line-height: 1.33;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
color: $gray-200;
display: inline-block;
}

View File

@@ -17,7 +17,7 @@
</span>
<div>
<span
class="svg-icon close-icon icon-16 color"
class="svg-icon icon-12 close-icon"
aria-hidden="true"
tabindex="0"
@click="hideDialog()"
@@ -45,13 +45,6 @@
:sprites-margin="'0px auto 0px -24px'"
/>
</div>
<item
v-else-if="item.key === 'gem'"
class="flat bordered-item"
:item="item"
:item-content-class="item.class"
:show-popover="false"
/>
<item
v-else-if="item.key != 'gem'"
class="flat bordered-item"
@@ -60,20 +53,10 @@
:show-popover="false"
/>
</slot>
<div
v-if="!showAvatar && user.items[item.purchaseType]"
class="owned"
:class="totalOwned"
>
<!-- eslint-disable-next-line max-len -->
<span class="owned-text">{{ $t('owned') }}: <span class="user-amount">{{ totalOwned }}</span></span>
</div>
<h4 class="title">
{{ itemText }}
</h4>
<div class="item-notes">
{{ itemNotes }}
</div>
<div v-html="itemNotes"></div>
<slot
name="additionalInfo"
:item="item"
@@ -86,61 +69,60 @@
/>
</slot>
<div
v-if="item.value > 0 && !(item.key === 'gem' && gemsLeft < 1)"
v-if="item.value > 0"
class="purchase-amount"
>
<!-- this is where the pretty item cost element lives -->
<div class="item-cost">
<span
class="cost"
:class="getPriceClass()"
>
<span
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons[getPriceClass()]"
>
</span>
<span
:class="getPriceClass()"
>{{ item.value }}</span>
</span>
</div>
<div
v-if="showAmountToBuy(item)"
class="how-many-to-buy"
>
{{ $t('howManyToBuy') }}
<strong>{{ $t('howManyToBuy') }}</strong>
</div>
<div
v-if="showAmountToBuy(item)"
>
<number-increment
class="number-increment"
@updateQuantity="selectedAmountToBuy = $event"
/>
<div
:class="{'notEnough': notEnoughCurrency}"
class="total"
>
<span class="total-text">{{ $t('sendTotal') }}</span>
<div v-if="showAmountToBuy(item)">
<div class="box">
<input
v-model.number="selectedAmountToBuy"
class="form-control"
type="number"
min="0"
step="1"
>
</div>
<span :class="{'notEnough': notEnoughCurrency}">
<span
class="svg-icon total icon-24"
class="svg-icon inline icon-32"
aria-hidden="true"
v-html="icons[getPriceClass()]"
></span>
<span
class="total-text"
class="cost"
:class="getPriceClass()"
>{{ item.value * selectedAmountToBuy }}</span>
</div>
>{{ item.value }}</span>
</span>
</div>
<div
v-else
class="d-flex align-items-middle"
>
<span
class="svg-icon inline icon-32 ml-auto my-auto"
aria-hidden="true"
v-html="icons[getPriceClass()]"
></span>
<span
class="cost mr-auto my-auto"
:class="getPriceClass()"
>{{ item.value }}</span>
</div>
</div>
<div
v-if="item.key === 'gem' && gemsLeft < 1"
class="no-more-gems"
v-if="item.key === 'gem'"
class="gems-left"
>
<strong v-if="gemsLeft > 0">{{ gemsLeft }} {{ $t('gemsRemaining') }}</strong>
<strong v-if="gemsLeft === 0">{{ $t('maxBuyGems') }}</strong>
</div>
<div v-if="attemptingToPurchaseMoreGemsThanAreLeft">
{{ $t('notEnoughGemsToBuy') }}
</div>
<div
@@ -165,7 +147,7 @@
{{ $t('viewSubscriptions') }}
</button>
<button
v-else-if="!(item.key === 'gem' && gemsLeft < 1)"
v-else
class="btn btn-primary"
:disabled="item.key === 'gem' && gemsLeft === 0 ||
attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid || item.locked ||
@@ -183,7 +165,6 @@
<countdown-banner
v-if="item.event && item.owned == null"
:end-date="endDate"
class="limitedTime available"
/>
<div
v-if="item.key === 'rebirth_orb' && item.value > 0 && user.stats.lvl >= 100"
@@ -197,32 +178,13 @@
<span v-html="$t('nextFreeRebirth', {days: nextFreeRebirth})"></span>
</div>
</div>
<div
v-if="item.key === 'gem'"
class="d-flex justify-content-center align-items-center"
>
<div
v-if="gemsLeft > 0"
class="gems-left d-flex justify-content-center align-items-center"
>
<strong>{{ $t('monthlyGems') }} &nbsp;</strong>
{{ gemsLeft }} / {{ totalGems }} {{ $t('gemsRemaining') }}
</div>
<div
v-if="gemsLeft === 0"
class="out-of-gems-banner d-flex justify-content-center align-items-center"
>
<strong>{{ $t('monthlyGems') }} &nbsp;</strong>
{{ gemsLeft }} / {{ totalGems }} {{ $t('gemsRemaining') }}
</div>
</div>
<div
slot="modal-footer"
class="clearfix"
class="d-flex"
>
<span class="user-balance float-left">{{ $t('yourBalance') }}</span>
<span class="balance mr-auto">{{ $t('yourBalance') }}</span>
<balanceInfo
class="currency-totals"
class="ml-auto"
:currency-needed="getPriceClass()"
:amount-needed="item.value"
/>
@@ -238,47 +200,11 @@
@include centeredModal();
.modal-body {
padding-left: 0px;
padding-right: 0px;
padding-bottom: 0px;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
margin: 24px 0 0 0;
padding: 16px 24px;
align-content: center;
.user-balance {
width: 150px;
height: 16px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
margin-left: -4px;
}
.currency-totals {
margin-right: -8px;
float: right;
}
}
.modal-dialog {
width: 448px;
box-sizing: border-box;
}
.badge-dialog {
left: -8px;
top: -8px;
width: 330px;
}
.avatar {
@@ -286,71 +212,8 @@
margin: 0 auto;
}
.owned {
height: 32px;
width: 141px;
margin-top: -36px;
margin-left: 153px;
padding-top: 6px;
background-color: $gray-600;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
display: block;
text-align: center;
position: relative;
z-index: 1;
.owned-text {
font-size: 0.75rem;
font-weight: bold;
line-height: 1.71;
}
.user-amount {
font-weight: normal !important;
}
}
.item {
width: 141px;
height: 147px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
cursor: default;
}
.item-content {
transform: scale(1.45, 1.45);
top: -25.67px;
left: 1px;
&.shop_gem {
transform: scale(1.45, 1.45);
top: -2px;
left: 0px;
}
}
.title {
height: 28px;
color: $gray-10;
font-size: 1.25rem;
margin-top: 25px;
}
.item-notes {
margin-top: 8px;
padding-left: 48.5px;
padding-right: 48.5px;
line-height: 1.71;
font-size: 0.875rem;
}
.content {
text-align: center;
width: 448px;
}
.item-wrapper {
@@ -358,22 +221,15 @@
}
.inner-content {
margin: 32px auto auto;
}
.btn-primary {
margin-top: 16px;
margin: 33px auto auto;
width: 282px;
}
.purchase-amount {
margin-top: 0px;
margin-top: 24px;
.how-many-to-buy {
font-weight: bold !important;
}
.number-increment {
margin-top: 16px;
margin-bottom: 16px;
}
.box {
@@ -399,105 +255,31 @@
}
}
}
.no-more-gems {
color: $yellow-5;
font-size: 0.875em;
line-height: 1.33;
margin: 16px 48px 0 48px;
}
// for cost icon of a single item
span.svg-icon.inline.icon-24 {
display: inline-block;
height: 24px;
width: 24px;
margin-right: 4px;
padding-top: 4px;
}
// for the total user cost
span.svg-icon.total.icon-24 {
display: inline-block;
height: 24px;
width: 24px;
margin-left: 6px;
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-right: 8px;
padding-top: 6px;
}
span.svg-icon.icon-16 {
height: 16px;
width: 16px;
}
.close-icon {
color: $gray-200;
stroke-width: 0px;
&:hover {
color: $gray-100;
}
}
.attributes-group {
margin: 32px;
border-radius: 4px;
line-height: 1.71;
font-size: 0.875;
}
.attributesGrid {
margin-top: 28px;
border-radius: 2px;
background-color: $gray-500;
}
.item-cost {
display: inline-flex;
margin: 16px 0;
align-items: center;
height: 40px;
vertical-align: middle;
}
.cost {
display: inline-block;
font-family: sans-serif;
font-size: 1.25rem;
width: 28px;
height: 32px;
font-size: 24px;
font-weight: bold;
padding: 6px 20px;
line-height: 1.4;
border-radius: 20px;
line-height: 1.33;
vertical-align: middle;
&.gems {
color: $green-10;
background-color: rgba(36, 204, 143, 0.15);
align-items: center;
color: $gems-color;
}
&.gold {
color: $yellow-5;
background-color: rgba(255, 190, 93, 0.15);
align-items: center;
}
&.hourglasses {
color: $hourglass-color;
background-color: rgba(41, 149, 205, 0.15);
align-items: center;
}
}
.total {
font-weight: bold;
font-size: 0.875rem;
padding-top: 2px;
margin-top: 4px;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-5;
color: $gold-color;
}
&.hourglasses {
@@ -505,84 +287,62 @@
}
}
.total-text {
color: $gray-50;
font-weight: bold;
font-size: 0.875rem;
line-height: 1.71;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-5;
}
&.hourglasses {
color: $hourglass-color;
}
}
button.btn.btn-primary {
margin-top: 16px;
padding: 4px 16px;
height: 32px;
margin-top: 24px;
margin-bottom: 24px;
min-width: 6rem;
&:focus {
border: 2px solid black;
}
}
.notEnough {
pointer-events: none;
opacity: 0.55;
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
}
.free-rebirth {
background-color: $yellow-5;
color: $white;
height: 2rem;
line-height: 16px;
margin: auto -1rem -1rem;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
}
// .pt-015 {
// padding-top: 0.15rem;
// }
.notEnough {
pointer-events: none;
opacity: 0.55;
}
.attributesGrid {
margin-top: 8px;
border-radius: 2px;
background-color: $gray-500;
margin: 10px 0 24px;
}
.gems-left {
height: 32px;
background-color: $green-100;
font-size: 0.75rem;
margin-top: 24px;
color: $green-1;
width: 100%;
margin-bottom: -24px;
margin-top: .5em;
}
.out-of-gems-banner {
height: 32px;
font-size: 0.75rem;
margin-top: 24px;
background-color: $yellow-100;
color: $yellow-1;
width: 100%;
margin-bottom: -24px;
}
.limitedTime {
height: 32px;
width: 446px;
font-size: 0.75rem;
margin: 24px 0 0 0;
background-color: $purple-300;
.free-rebirth {
background-color: $yellow-5;
color: $white;
margin-bottom: -24px;
height: 2rem;
line-height: 16px;
margin: auto -1rem -1rem;
}
.pt-015 {
padding-top: 0.15rem;
}
}
</style>
<style lang="scss" scoped>
@@ -610,8 +370,6 @@ import svgGem from '@/assets/svg/gem.svg';
import svgHourglasses from '@/assets/svg/hourglass.svg';
import svgClock from '@/assets/svg/clock.svg';
import svgWhiteClock from '@/assets/svg/clock-white.svg';
import svgPositive from '@/assets/svg/positive.svg';
import svgNegative from '@/assets/svg/negative.svg';
import BalanceInfo from './balanceInfo.vue';
import PinBadge from '@/components/ui/pinBadge';
@@ -619,7 +377,6 @@ import CountdownBanner from './countdownBanner';
import currencyMixin from './_currencyMixin';
import notifications from '@/mixins/notifications';
import buyMixin from '@/mixins/buy';
import numberIncrement from '@/components/shared/numberIncrement';
import { mapState } from '@/libs/store';
@@ -650,17 +407,14 @@ export default {
Avatar,
PinBadge,
CountdownBanner,
numberIncrement,
},
mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin],
props: {
// eslint-disable-next-line vue/require-default-prop
item: {
type: Object,
},
priceType: {
type: String,
default: '',
},
withPin: {
type: Boolean,
@@ -679,14 +433,10 @@ export default {
hourglasses: svgHourglasses,
clock: svgClock,
whiteClock: svgWhiteClock,
positive: svgPositive,
negative: svgNegative,
}),
selectedAmountToBuy: 1,
selectedAmount: 1,
isPinned: false,
quantity: 1,
};
},
computed: {
@@ -724,11 +474,6 @@ export default {
return planGemLimits.convCap
+ this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
},
totalGems () {
if (!this.user.purchased.plan) return 0;
return planGemLimits.convCap
+ this.user.purchased.plan.consecutive.gemCapExtra;
},
attemptingToPurchaseMoreGemsThanAreLeft () {
if (this.item && this.item.key && this.item.key === 'gem' && this.selectedAmountToBuy > this.gemsLeft) return true;
return false;
@@ -745,9 +490,6 @@ export default {
endDate () {
return moment(this.item.event.end);
},
totalOwned () {
return this.user.items[this.item.purchaseType][this.item.key] || 0;
},
},
watch: {
item: function itemChanged () {
@@ -758,9 +500,7 @@ export default {
methods: {
onChange ($event) {
this.$emit('change', $event);
this.selectedAmountToBuy = 1;
},
buyItem () {
// @TODO: I think we should buying to the items.
// Turn the items into classes, and use polymorphism
@@ -857,7 +597,6 @@ export default {
}
},
hideDialog () {
this.selectedAmountToBuy = 1;
this.$root.$emit('bv::hide::modal', 'buy-modal');
},
getPriceClass () {

View File

@@ -16,6 +16,9 @@
.limitedTime {
height: 32px;
width: calc(100% + 30px);
margin: 0 -15px; // the modal content has its own padding
font-size: 12px;
line-height: 1.33;
text-align: center;

View File

@@ -4,9 +4,9 @@
:hide-header="true"
@change="onChange($event)"
>
<div>
<div class="close">
<span
class="svg-icon close-icon icon-16 color"
class="svg-icon inline icon-10"
aria-hidden="true"
@click="hideDialog()"
v-html="icons.close"
@@ -14,73 +14,60 @@
</div>
<div
v-if="item"
class="content bordered-item"
class="content"
>
<div class="inner-content">
<item
class="flat bordered-item"
class="flat"
:item="item"
:item-content-class="itemContextToSell.itemClass"
:show-popover="false"
/>
<span class="owned">
{{ $t('owned') }}: <span class="user-amount">{{ itemContextToSell.itemCount }}</span>
</span>
>
<countBadge
slot="itemBadge"
:show="true"
:count="itemContextToSell.itemCount"
/>
</item>
<h4 class="title">
{{ itemContextToSell.itemName }}
</h4>
<div v-if="item.key === 'Saddle'">
<div class="item-notes">
<div class="text">
{{ item.sellWarningNote() }}
</div>
<br>
</div>
<div v-else>
<div>
<div class="item-notes">
<div class="text">
{{ item.notes() }}
</div>
<div class="item-cost">
<span class="cost gold">
<span
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons.gold"
></span>
<span>{{ item.value }}</span>
</span>
<div>
<b class="how-many-to-sell">{{ $t('howManyToSell') }}</b>
</div>
<div>
<span
class="how-many-to-sell"
>
{{ $t('howManyToSell') }}
</span>
</div>
<div>
<number-increment
@updateQuantity="selectedAmountToSell = $event"
<b-input
v-model="selectedAmountToSell"
class="itemsToSell"
type="number"
:max="itemContextToSell.itemCount"
min="1"
step="1"
@keyup.native="preventNegative($event)"
/>
</div>
<div class="total-row">
<span class="total-text">
{{ $t('sendTotal') }}
</span>
<span
class="svg-icon total icon-24"
class="svg-icon inline icon-32"
aria-hidden="true"
v-html="icons.gold"
></span>
<span class="total-text gold">
{{ item.value * selectedAmountToSell }}
</span>
<span class="value">{{ item.value }}</span>
</div>
<button
class="btn btn-primary"
:disabled="selectedAmountToSell > itemContextToSell.itemCount"
@click="sellItems()"
>
{{ $t('sellItems') }}
{{ $t('sell') }}
</button>
</div>
</div>
@@ -90,10 +77,8 @@
slot="modal-footer"
class="clearfix"
>
<span class="user-balance float-left">{{ $t('yourBalance') }}</span>
<balanceInfo
class="float-right currency-totals"
/>
<span class="balance float-left">{{ $t('yourBalance') }}</span>
<balanceInfo class="float-right" />
</div>
</b-modal>
</template>
@@ -110,13 +95,51 @@
}
.modal-dialog {
width: 448px;
width: 330px;
}
.modal-body {
padding-left: 0px;
padding-right: 0px;
padding-bottom: 0px;
.content {
text-align: center;
}
.inner-content {
margin: 33px auto auto;
width: 282px;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-left: 24px;
margin-right: 8px;
vertical-align: middle;
}
.value {
width: 28px;
height: 32px;
font-size: 24px;
font-weight: bold;
line-height: 1.33;
color: #df911e;
vertical-align: middle;
}
button.btn.btn-primary {
margin-top: 24px;
margin-bottom: 24px;
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
}
.modal-footer {
@@ -125,215 +148,29 @@
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
margin: 24px 0 0;
padding: 16px 24px;
align-content: center;
.user-balance {
width: 150px;
height: 16px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
margin-left: -4px;
}
.currency-totals {
margin-right: -8px;
float: right;
}
}
.content {
text-align: center;
}
.inner-content {
margin: 33px auto auto;
width: 282px;
}
.owned {
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
background-color: $gray-600;
padding: 8px 8px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
.how-many-to-sell {
margin-bottom: 16px;
display: block;
width: 141px;
margin-left: 71px;
margin-top: -48px;
position: relative;
z-index: 1;
.user-amount {
font-weight: normal !important;
}
}
.item-wrapper {
margin-top: -10px;
}
.item {
width: 141px;
height: 147px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
cursor: default;
margin-top: 8px;
}
.item-content {
transform: scale(1.45, 1.45);
top: -25px;
left: 1px;
}
.title {
color: $gray-10;
font-size: 1.25rem;
margin-top: 26px;
margin-bottom: 0px;
}
.item-notes {
margin-top: 12px;
line-height: 1.71;
font-size: 0.875rem;
}
// for cost icon of a single item
span.svg-icon.inline.icon-24 {
display: inline-block;
height: 24px;
width: 24px;
margin-right: 4px;
padding-top: 4px;
}
// for the total user cost
span.svg-icon.total.icon-24 {
display: inline-block;
height: 24px;
width: 24px;
margin-left: 6px;
margin-right: 8px;
padding-top: 6px;
}
span.svg-icon.icon-16 {
height: 16px;
width: 16px;
}
.close-icon {
color: $gray-200;
stroke-width: 0px;
cursor: pointer;
&:hover {
color: $gray-100;
}
}
.item-cost {
display: inline-flex;
margin: 16px 0;
align-items: center;
height: 40px;
}
.cost {
display: inline-block;
font-family: sans-serif;
font-size: 1.25rem;
font-weight: bold;
padding: 6px 20px;
line-height: 1.4;
border-radius: 20px;
&.gold {
color: $yellow-5;
background-color: rgba(255, 190, 93, 0.15);
align-items: center;
}
}
}
.how-many-to-sell {
font-weight: bold !important;
}
.number-increment {
margin-top: 16px;
}
.total-row {
font-weight: bold;
font-size: 0.875rem;
margin-top: 16px;
&.gold {
color: $yellow-5;
}
}
.total-text {
color: $gray-50;
font-weight: bold;
font-size: 0.875rem;
line-height: 1.71;
&.gold {
color: $yellow-5;
}
}
button.btn.btn-primary {
margin-top: 16px;
padding: 4px 16px;
height: 32px;
&:focus {
border: 2px solid black;
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
}
}
</style>
<script>
import svgClose from '@/assets/svg/close.svg';
import svgGold from '@/assets/svg/gold.svg';
import svgGem from '@/assets/svg/gem.svg';
import svgPositive from '@/assets/svg/positive.svg';
import svgNegative from '@/assets/svg/negative.svg';
import BalanceInfo from '../balanceInfo.vue';
import Item from '@/components/inventory/item';
import numberIncrement from '@/components/shared/numberIncrement';
import CountBadge from '@/components/ui/countBadge';
export default {
components: {
BalanceInfo,
Item,
numberIncrement,
CountBadge,
},
data () {
return {
@@ -344,8 +181,6 @@ export default {
close: svgClose,
gold: svgGold,
gem: svgGem,
svgPositive,
svgNegative,
}),
};
},
@@ -376,10 +211,6 @@ export default {
this.selectedAmountToSell = 0;
}
},
maxOwned () {
const maxOwned = this.itemContextToSell.itemCount;
return maxOwned;
},
sellItems () {
if (!Number.isInteger(Number(this.selectedAmountToSell))) {
this.selectedAmountToSell = 0;

View File

@@ -33,22 +33,6 @@
v-if="!item.locked"
class="purchase-amount"
>
<div class="item-cost">
<span
class="cost"
:class="priceType"
>
<span
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons[priceType]"
>
</span>
<span
:class="priceType"
>{{ item.value }}</span>
</span>
</div>
<div class="how-many-to-buy">
<strong>{{ $t('howManyToBuy') }}</strong>
</div>
@@ -58,25 +42,24 @@
>
{{ item.addlNotes }}
</div>
<div>
<number-increment
@updateQuantity="selectedAmountToBuy = $event"
/>
</div>
<div class="total-row">
<span class="total-text">
{{ $t('sendTotal') }}
</span>
<span
class="svg-icon inline icon-20"
aria-hidden="true"
v-html="currencyIcon"
></span>
<span
class="total"
:class="priceType"
>{{ item.value * selectedAmountToBuy }}</span>
<div class="box">
<input
v-model.number="selectedAmountToBuy"
class="form-control"
type="number"
min="0"
step="1"
>
</div>
<span
class="svg-icon inline icon-32"
aria-hidden="true"
v-html="currencyIcon"
></span>
<span
class="value"
:class="priceType"
>{{ item.value }}</span>
</div>
<button
v-if="priceType === 'gems'
@@ -89,7 +72,7 @@
</button>
<button
v-else
class="btn btn-primary mb-4"
class="btn btn-primary"
:class="{'notEnough': !enoughCurrency(priceType, item.value * selectedAmountToBuy)}"
:disabled="numberInvalid"
@click="buyItem()"
@@ -129,39 +112,6 @@
margin-top: 1rem;
}
.modal-body {
padding-left: 0px;
padding-right: 0px;
padding-bottom: 0px;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
padding: 16px 24px;
align-content: center;
.user-balance {
width: 150px;
height: 16px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
margin-left: -4px;
}
.currency-totals {
margin-right: -8px;
float: right;
}
}
.modal-dialog {
margin-top: 8%;
width: 448px !important;
@@ -179,13 +129,8 @@
margin: 33px auto auto;
}
.item-notes {
height: 48px;
margin-top: 8px;
padding-left: 48.5px;
padding-right: 48.5px;
line-height: 1.71;
font-size: 0.875rem;
.modal-body {
padding-bottom: 0px;
}
.questInfo {
@@ -207,14 +152,16 @@
height: 100%;
}
button.btn.btn-primary {
margin-top: 14px;
padding: 4px 16px;
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-right: 8px;
vertical-align: middle;
}
&:focus {
border: 2px solid black;
}
button.btn.btn-primary {
margin-top: 24px;
margin-bottom: 24px;
}
.balance {
@@ -226,6 +173,19 @@
color: $gray-200;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
padding: 1rem 1.5rem;
&> * {
margin: 0;
}
}
.notEnough {
pointer-events: none;
opacity: 0.55;
@@ -238,108 +198,30 @@
margin-bottom: 16px;
}
.item-cost {
padding-bottom: 16px;
.box {
display: inline-block;
width: 74px;
height: 40px;
border-radius: 2px;
background-color: #ffffff;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
margin-right: 24px;
input {
width: 100%;
border: none;
}
.cost {
height: 40px;
font-size: 1.25rem;
font-weight: bold;
vertical-align: middle;
padding: 8px 20px 8px 20px;
&.gems {
color: $green-10;
background-color: rgba(36, 204, 143, 0.15);
line-height: 1.4;
margin: 0 0 0 -4px;
border-radius: 20px;
}
&.gold {
color: $yellow-5;
background-color: rgba(255, 190, 93, 0.15);
line-height: 1.4;
margin: 0 0 0 -4px;
border-radius: 20px;
}
&.hourglasses {
color: $hourglass-color;
background-color: rgba(41, 149, 205, 0.15);
line-height: 1.4;
margin: 0 0 0 -4px;
border-radius: 20px;
}
}
.total-row {
font-weight: bold;
font-size: 0.875rem;
margin-top: 16px;
}
.total {
font-weight: bold;
font-size: 0.875rem;
margin-top: 16px;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-5;
}
&.hourglasses {
color: $hourglass-color;
input::-webkit-contacts-auto-fill-button {
visibility: hidden;
display: none !important;
pointer-events: none;
position: absolute;
right: 0;
}
}
}
.total-text {
color: $gray-50;
font-weight: bold;
font-size: 0.875rem;
height: 24px;
line-height: 1.71;
padding-right: 4px;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-5;
}
&.hourglasses {
color: $hourglass-color;
}
}
span.svg-icon.inline.icon-20 {
height: 20px;
width: 20px;
margin-right: 4px;
vertical-align: middle;
}
span.svg-icon.inline.icon-24 {
height: 24px;
width: 24px;
margin-right: 8px;
vertical-align: middle;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-right: 8px;
vertical-align: middle;
}
@media only screen and (max-width: 1000px) {
.modal-dialog {
max-width: 80%;
@@ -352,10 +234,9 @@
}
}
}
}
</style>
<!-- <style lang="scss" scoped>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.value {
@@ -379,7 +260,7 @@
color: $hourglass-color;
}
}
</style> -->
</style>
<script>
import moment from 'moment';
@@ -391,8 +272,6 @@ import svgExperience from '@/assets/svg/experience.svg';
import svgGem from '@/assets/svg/gem.svg';
import svgGold from '@/assets/svg/gold.svg';
import svgHourglasses from '@/assets/svg/hourglass.svg';
import svgPositive from '@/assets/svg/positive.svg';
import svgNegative from '@/assets/svg/negative.svg';
import BalanceInfo from '../balanceInfo.vue';
import currencyMixin from '../_currencyMixin';
@@ -401,7 +280,6 @@ import buyMixin from '@/mixins/buy';
import numberInvalid from '@/mixins/numberInvalid';
import PinBadge from '@/components/ui/pinBadge';
import CountdownBanner from '../countdownBanner';
import numberIncrement from '@/components/shared/numberIncrement';
import questDialogContent from './questDialogContent';
import QuestRewards from './questRewards';
@@ -415,7 +293,6 @@ export default {
PinBadge,
questDialogContent,
CountdownBanner,
numberIncrement,
},
mixins: [buyMixin, currencyMixin, notifications, numberInvalid],
props: {
@@ -424,7 +301,6 @@ export default {
},
priceType: {
type: String,
default: '',
},
withPin: {
type: Boolean,
@@ -436,11 +312,9 @@ export default {
clock: svgClock,
close: svgClose,
experience: svgExperience,
gems: svgGem,
gem: svgGem,
gold: svgGold,
hourglasses: svgHourglasses,
positive: svgPositive,
negative: svgNegative,
hourglass: svgHourglasses,
}),
isPinned: false,
@@ -465,8 +339,8 @@ export default {
},
currencyIcon () {
if (this.priceType === 'gold') return this.icons.gold;
if (this.priceType === 'hourglasses') return this.icons.hourglasses;
return this.icons.gems;
if (this.priceType === 'hourglasses') return this.icons.hourglass;
return this.icons.gem;
},
endDate () {
return moment(this.item.event.end);

View File

@@ -33,17 +33,17 @@
h3 {
color: $gray-10;
margin-bottom: 4pxrem;
margin-bottom: 0.25rem;
}
.quest-image {
margin: 0 auto;
margin-bottom: 16px;
margin-top: 24px;
margin-bottom: 1em;
margin-top: 1.5em;
}
.text {
margin: 16px 16px;
margin-bottom: 1rem;
overflow-y: auto;
text-overflow: ellipsis;
}
@@ -54,10 +54,10 @@
line-height: 1.71;
color: $gray-50;
text-align: center;
margin-bottom: 8px;
margin-bottom: 0.5rem;
::v-deep .user-label {
font-size: 0.875rem;
font-size: 14px;
}
}

View File

@@ -177,6 +177,9 @@ export default {
@import '~@/assets/scss/colors.scss';
.quest-rewards {
margin-left: -1rem;
margin-right: -1rem;
background-color: $gray-700;
}

View File

@@ -106,7 +106,6 @@ export default {
preventMultipleWatchExecution: false,
eventPromoBannerHeight: null,
sleepingBannerHeight: null,
warningBannerHeight: null,
};
},
computed: {
@@ -136,10 +135,6 @@ export default {
notificationBannerHeight () {
let scrollPosToCheck = 56;
if (this.warningBannerHeight) {
scrollPosToCheck += this.warningBannerHeight;
}
if (this.sleepingBannerHeight) {
scrollPosToCheck += this.sleepingBannerHeight;
}
@@ -366,7 +361,6 @@ export default {
updateBannerHeightAndScrollY () {
this.updateEventBannerHeight();
this.warningBannerHeight = getBannerHeight('chat-warning');
this.sleepingBannerHeight = getBannerHeight('damage-paused');
this.updateScrollY();
},

View File

@@ -1,615 +0,0 @@
<template>
<div class="top-container mx-auto">
<div class="main-text mr-4">
<!-- title goes here -->
<div class="title-details">
<h1 v-once>
{{ $t('sunsetFaqTitle') }}
</h1>
</div>
<div class="body-text">
<p v-html="$t('sunsetFaqPara1')"></p> <!-- there's html in here -->
<p>{{ $t('sunsetFaqPara2') }}</p>
<p>{{ $t('sunsetFaqPara3') }}</p>
<p>{{ $t('sunsetFaqPara4') }}</p>
<p>{{ $t('sunsetFaqPara5') }}</p>
</div>
<!-- Which services are ending -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader1') }}
</h3>
</div>
<div class="body-text">
<p v-html="$t('sunsetFaqPara6')"></p>
</div>
<!-- Why are tavern and guild ending? -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader2') }}
</h3>
</div>
<div class="body-text">
<ul>
<li>{{ $t('sunsetFaqList1') }}</li>
<li>{{ $t('sunsetFaqList2') }}</li>
<li>{{ $t('sunsetFaqList3') }}</li>
</ul>
</div>
<!-- Can I still talk to my party/group members? -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader3') }}
</h3>
</div>
<div class="body-text">
<p>{{ $t('sunsetFaqPara7') }}</p>
</div>
<!-- Pausing dailies -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader4') }}
</h3>
</div>
<div class="body-text">
<p>{{ $t('sunsetFaqPara8') }}</p>
</div>
<!-- Accessing group plans -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader5') }}
</h3>
</div>
<div class="body-text">
<p v-html="$t('sunsetFaqPara9')"></p> <!-- there's html in here -->
</div>
<!-- Can I access guild chats? Or banked Gems? -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader12') }}
</h3>
</div>
<div class="body-text">
<p v-html="$t('sunsetFaqPara21')"></p>
</div>
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader6') }}
</h3>
</div>
<div class="body-text">
<p>{{ $t('sunsetFaqPara10') }}</p>
</div>
<!-- How can players find groups? -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader7') }}
</h3>
</div>
<div class="body-text">
<p>{{ $t('sunsetFaqPara11') }}</p>
</div>
<!-- What about contributors? -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader8') }}
</h3>
</div>
<div class="body-text">
<p v-html="$t('sunsetFaqPara12')"></p> <!-- there's html in here -->
<p v-html="$t('sunsetFaqPara13')"></p> <!-- there's html in here -->
<p v-html="$t('sunsetFaqPara14')"></p> <!-- there's html in here -->
<p v-html="$t('sunsetFaqPara15')"></p> <!-- there's html in here -->
<p v-html="$t('sunsetFaqPara16')"></p> <!-- there's html in here -->
<p v-html="$t('sunsetFaqPara17')"></p> <!-- there's html in here -->
<p v-html="$t('sunsetFaqPara18')"></p> <!-- there's html in here -->
<p v-html="$t('sunsetFaqPara19')"></p> <!-- there's html in here -->
</div>
<!-- Challenges -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader9') }}
</h3>
</div>
<div class="body-text">
<ul>
<li>{{ $t('sunsetFaqList4') }}</li>
<li>{{ $t('sunsetFaqList5') }}</li>
<li>{{ $t('sunsetFaqList6') }}</li>
<li>{{ $t('sunsetFaqList7') }}</li>
</ul>
</div>
<!-- Questions about how to use Habitica -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader10') }}
</h3>
</div>
<div class="body-text">
<ul>
<li v-html="$t('sunsetFaqList8')"></li> <!-- there's html in here -->
<li v-html="$t('sunsetFaqList9')"></li> <!-- there's html in here -->
<li v-html="$t('sunsetFaqList10')"></li> <!-- there's html in here -->
</ul>
</div>
<!-- Community Guidelines and TOS -->
<div>
<h3 class="headings">
{{ $t('sunsetFaqHeader11') }}
</h3>
</div>
<div class="body-text">
<p v-html="$t('sunsetFaqPara20')"></p>
</div>
</div>
<!-- sidebar -->
<div class="sidebar py-4 d-flex flex-column">
<!-- staff -->
<div class="ml-4">
<h2>
{{ $t('staff') }}
</h2>
<div class="d-flex flex-wrap">
<div
v-for="user in staff"
:key="user.uuid"
class="staff col-6 p-0"
>
<div class="d-flex">
<router-link
class="title"
:to="{'name': 'userProfile', 'params': {'userId': user.uuid}}"
>
{{ user.name }}
</router-link>
<div
v-if="user.type === 'Staff'"
class="svg-icon staff-icon ml-1"
v-html="icons.tierStaff"
></div>
</div>
</div>
</div>
</div>
<!-- player tiers -->
<div class="ml-4">
<h2 class="mt-4 mb-1">
{{ $t('playerTiers') }}
</h2>
<ul class="tier-list">
<li
v-once
class="tier1 d-flex justify-content-center"
>
{{ $t('tier1') }}
<div
class="svg-icon ml-1"
v-html="icons.tier1"
></div>
</li>
<li
v-once
class="tier2 d-flex justify-content-center"
>
{{ $t('tier2') }}
<div
class="svg-icon ml-1"
v-html="icons.tier2"
></div>
</li>
<li
v-once
class="tier3 d-flex justify-content-center"
>
{{ $t('tier3') }}
<div
class="svg-icon ml-1"
v-html="icons.tier3"
></div>
</li>
<li
v-once
class="tier4 d-flex justify-content-center"
>
{{ $t('tier4') }}
<div
class="svg-icon ml-1"
v-html="icons.tier4"
></div>
</li>
<li
v-once
class="tier5 d-flex justify-content-center"
>
{{ $t('tier5') }}
<div
class="svg-icon ml-1"
v-html="icons.tier5"
></div>
</li>
<li
v-once
class="tier6 d-flex justify-content-center"
>
{{ $t('tier6') }}
<div
class="svg-icon ml-1"
v-html="icons.tier6"
></div>
</li>
<li
v-once
class="tier7 d-flex justify-content-center"
>
{{ $t('tier7') }}
<div
class="svg-icon ml-1"
v-html="icons.tier7"
></div>
</li>
<li
v-once
class="moderator d-flex justify-content-center"
>
{{ $t('tierModerator') }}
<div
class="svg-icon ml-1"
v-html="icons.tierMod"
></div>
</li>
<li
v-once
class="staff d-flex justify-content-center"
>
{{ $t('tierStaff') }}
<div
class="svg-icon ml-1"
v-html="icons.tierStaff"
></div>
</li>
<li
v-once
class="npc d-flex justify-content-center"
>
{{ $t('tierNPC') }}
</li>
</ul>
</div>
<!-- Daniel in sweet, sweet retirement with Jorts -->
<div>
<div class="gradient">
</div>
<div
class="grassy-meadow-backdrop"
:style="{'background-image': imageURLs.background}"
>
<div
class="daniel_front"
:style="{'background-image': imageURLs.npc}"
></div>
<div
class="pixel-border"
:style="{'background-image': imageURLs.pixel_border}"
></div>
</div>
</div>
<!-- email admin -->
<div class="d-flex flex-column justify-content-center">
<div class="question mx-auto">
{{ $t('anotherQuestion') }}
</div>
<div
class="contact mx-auto"
>
<p v-html="$t('contactAdmin')"></p> <!-- there's html in here -->
</div>
</div>
</div>
<!-- final div! -->
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
h1 {
margin-top: 0px;
line-height: 1.33;
}
li {
padding-bottom: 16px;
&::marker {
size: 0.5em;
}
}
p {
margin-bottom: 21px;
}
ul {
padding-left: 20px;
}
.top-container {
width: 66.67%;
margin-top: 80px;
display: flex;
@media (max-width: 1024px) {
flex-wrap: wrap;
}
}
.main-text {
.body-text {
font-size: 1em;
color: $gray-10;
line-height: 1.71;
}
.headings {
font-size: 1.15em;
font-weight: 400;
line-height: 1.75;
color: $purple-200;
}
}
.sidebar {
height: fit-content;
width: 330px;
background-color: $gray-700;
border-radius: 16px;
h2 {
color: $gray-10;
font-family: Roboto;
font-size: 14px;
font-weight: bold;
line-height: 1.71;
}
.staff {
.staff-icon {
width: 10px;
margin-top: 5px;
}
.title {
height: 24px;
color: $purple-300;
font-weight: bold;
display: inline-block;
margin-bottom: 4px;
}
}
.tier-list {
list-style-type: none;
padding: 0;
width: 282px;
font-size: 1em !important;
li {
height: 40px;
border-radius: 4px;
border: solid 1px $gray-500;
text-align: center;
padding: 8px 0;
margin-bottom: 8px;
margin-right: 4px;
font-weight: bold;
line-height: 1.71;
}
.tier1 {
color: #c42870;
.svg-icon {
width: 11px;
margin-top: 5px;
}
}
.tier2 {
color: #b01515;
.svg-icon {
width: 11px;
margin-top: 5px;
}
}
.tier3 {
color: #d70e14;
.svg-icon {
width: 13px;
margin-top: 4px;
}
}
.tier4 {
color: #c24d00;
.svg-icon {
width: 13px;
margin-top: 4px;
}
}
.tier5 {
color: #9e650f;
.svg-icon {
width: 8px;
margin-top: 7px;
}
}
.tier6 {
color: #2b8363;
.svg-icon {
width: 8px;
margin-top: 7px;
}
}
.tier7 {
color: #167e87;
.svg-icon {
width: 12px;
margin-top: 4px;
}
}
.moderator {
color: #277eab;
.svg-icon {
width: 13px;
margin-top: 3px;
}
}
.staff {
color: #6133b4;
.svg-icon {
width: 10px;
margin-top: 7px;
}
}
.npc {
color: $black;
}
}
.gradient {
position: absolute;
width: 330px;
height: 100px;
margin: -1px 0 116px;
background-image: linear-gradient(to bottom, $gray-700 0%, rgba(249, 249, 249, 0) 100%);
}
.grassy-meadow-backdrop {
background-repeat: repeat-x;
width: 330px;
height: 246px;
}
.daniel_front {
height: 246px;
width: 330px;
background-repeat: no-repeat;
margin: 0 auto;
}
.pixel-border {
width: 330px;
height: 30px;
background-repeat: no-repeat;
position: absolute;
margin-top: -30px;
}
.question {
font-size: 1em;
font-weight: bold;
line-height: 1.71;
color: $gray-10;
margin-top: 24px;
}
.contact p {
font-size: 1em;
margin-bottom: 0px;
}
}
</style>
<script>
import find from 'lodash/find';
import { mapState } from '@/libs/store';
import tier1 from '@/assets/svg/tier-1.svg';
import tier2 from '@/assets/svg/tier-2.svg';
import tier3 from '@/assets/svg/tier-3.svg';
import tier4 from '@/assets/svg/tier-4.svg';
import tier5 from '@/assets/svg/tier-5.svg';
import tier6 from '@/assets/svg/tier-6.svg';
import tier7 from '@/assets/svg/tier-7.svg';
import tierMod from '@/assets/svg/tier-mod.svg';
import tierNPC from '@/assets/svg/tier-npc.svg';
import tierStaff from '@/assets/svg/tier-staff.svg';
import staffList from '../../libs/staffList';
export default {
data () {
return {
icons: Object.freeze({
tier1,
tier2,
tier3,
tier4,
tier5,
tier6,
tier7,
tierMod,
tierNPC,
tierStaff,
}),
group: {
chat: [],
},
sections: {
worldBoss: true,
},
staff: staffList,
};
},
computed: {
...mapState({
currentEventList: 'worldState.data.currentEventList',
}),
imageURLs () {
const currentEvent = find(this.currentEventList, event => Boolean(event.season));
if (!currentEvent) {
return {
background: 'url(/static/npc/normal/tavern_background.png)',
npc: 'url(/static/npc/normal/tavern_npc.png)',
pixel_border: 'url(/static/npc/normal/pixel_border.png)',
};
}
return {
background: `url(/static/npc/${currentEvent.season}/tavern_background.png)`,
npc: `url(/static/npc/${currentEvent.season}/tavern_npc.png)`,
pixel_border: 'url(/static/npc/normal/pixel_border.png)',
};
},
},
async mounted () {
this.$store.dispatch('common:setTitle', {
subSection: this.$t('faq/taverns-and-guilds'),
});
document.body.style.background = '#ffffff';
},
};
</script>

View File

@@ -7,19 +7,13 @@
<br>
<p class="text-center">
<button
id="buttonClearBrowserData"
class="btn btn-lg btn-danger"
popover-trigger="mouseover"
:popover="$t('localStorageClearExplanation')"
@click="clearLocalStorage()"
>
{{ $t('localStorageClear') }}
</button>
<b-popover
target="buttonClearBrowserData"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('localStorageClearExplanation')"
/>
</p>
<br>
<p v-html="$t('localStorageTryNext', localStorageTryNext) "></p>

View File

@@ -1,61 +1,99 @@
<template>
<div class="container-fluid w-75 mx-auto">
<div class="container-fluid">
<h1>{{ $t('communityGuidelines') }}</h1>
<hr>
<p>{{ $t('lastUpdated') }} June 8, 2023</p>
<p>{{ $t('lastUpdated') }} February 8, 2023</p>
<h2 id="welcome">
{{ $t('commGuideHeadingWelcome') }}
</h2>
<img
src="~@/assets/images/community-guidelines/intro.png"
class="mb-3"
>
<p v-html="$t('commGuidePara001')"></p>
<p v-html="$t('commGuidePara002')"></p>
<div class="media align-items-center">
<img src="~@/assets/images/community-guidelines/intro.png">
<div class="media-body">
<p v-html="$t('commGuidePara001')"></p>
<p v-html="$t('commGuidePara002')"></p>
</div>
</div>
<p v-html="$t('commGuidePara003')"></p>
<h2 id="interactions">
{{ $t('commGuideHeadingInteractions') }}
</h2>
<p v-html="$t('commGuidePara015')"></p>
<p v-html="$t('commGuidePara016')"></p>
<p v-html="$t('commGuidePara017')"></p>
<ul>
<li v-html="$t('commGuideList01F')"></li>
<li v-html="$t('commGuideList01A')"></li>
<li v-html="$t('commGuideList01B')"></li>
<li v-html="$t('commGuideList01C')"></li>
<li v-html="$t('commGuideList01D')"></li>
<li v-html="$t('commGuideList01E')"></li>
<li v-html="$t('commGuideList01F')"></li>
</ul>
<div class="media align-items-center">
<img src="~@/assets/images/community-guidelines/publicSpaces.png">
</div>
<ul>
<li><strong>{{ $t('commGuideList01A') }}</strong></li>
<li v-html="$t('commGuideList02C')"></li>
<li v-html="$t('commGuideList02N')"></li>
<li v-html="$t('commGuideList02H')"></li>
<li v-html="$t('commGuideList02A')"></li>
<li v-html="$t('commGuideList02I')"></li>
<li v-html="$t('commGuideList02B')"></li>
<li v-html="$t('commGuideList02G')"></li>
<li v-html="$t('commGuideList02C')"></li>
<li v-html="$t('commGuideList02D')"></li>
<li v-html="$t('commGuideList02E')"></li>
<li v-html="$t('commGuideList02O')"></li>
<li v-html="$t('commGuidePara037')"></li>
<li v-html="$t('commGuideList02P')"></li>
<li v-html="$t('commGuideList02Q')"></li>
<li v-html="$t('commGuideList02F')"></li>
<li v-html="$t('commGuideList02M')"></li>
<li v-html="$t('commGuideList02L')"></li>
<li v-html="$t('commGuideList02J')"></li>
<li v-html="$t('commGuideList02K')"></li>
<li v-html="$t('commGuideList02L')"></li>
</ul>
<p v-html="$t('commGuidePara019')"></p>
<p v-html="$t('commGuidePara020')"></p>
<p v-html="$t('commGuidePara020A')"></p>
<p v-html="$t('commGuidePara021')"></p>
<h3 id="tavern">
{{ $t('commGuideHeadingTavern') }}
</h3>
<div class="media align-items-center">
<img src="~@/assets/images/community-guidelines/tavern.png">
<div class="media-body">
<p v-html="$t('commGuidePara022')"></p>
<p v-html="$t('commGuidePara023')"></p>
<p v-html="$t('commGuidePara024')"></p>
</div>
</div>
<h3 id="guilds">
{{ $t('commGuideHeadingPublicGuilds') }}
</h3>
<div class="media align-items-center">
<div class="media-body">
<p v-html="$t('commGuidePara029')"></p>
<p v-html="$t('commGuidePara031')"></p>
</div>
<img src="~@/assets/images/community-guidelines/publicGuilds.png">
</div>
<p v-html="$t('commGuidePara033')"></p>
<p v-html="$t('commGuidePara035')"></p>
<p v-html="$t('commGuidePara036')"></p>
<p v-html="$t('commGuidePara037')"></p>
<p v-html="$t('commGuidePara038')"></p>
<h2 id="infractions-consequences-restoration">
{{ $t('commGuideHeadingInfractionsEtc') }}
</h2>
<h3 id="infractions">
{{ $t('commGuideHeadingInfractions') }}
</h3>
<img
src="~@/assets/images/community-guidelines/infractions.png"
class="mb-3"
>
<p v-html="$t('commGuidePara050')"></p>
<p v-html="$t('commGuidePara051')"></p>
<div class="media align-items-center">
<img src="~@/assets/images/community-guidelines/infractions.png">
<div class="media-body">
<p v-html="$t('commGuidePara050')"></p>
<p v-html="$t('commGuidePara051')"></p>
</div>
</div>
<h4>{{ $t('commGuideHeadingSevereInfractions') }}</h4>
<p v-html="$t('commGuidePara052')"></p>
<p v-html="$t('commGuidePara053')"></p>
<ul>
<li v-html="$t('commGuideList05A')"></li>
<li v-html="$t('commGuideList05B')"></li>
<li v-html="$t('commGuideList05C')"></li>
<li v-html="$t('commGuideList05D')"></li>
@@ -63,34 +101,59 @@
<li v-html="$t('commGuideList05F')"></li>
<li v-html="$t('commGuideList05G')"></li>
<li v-html="$t('commGuideList05H')"></li>
<li v-html="$t('commGuideList05A')"></li>
</ul>
<h4>{{ $t('commGuideHeadingModerateInfractions') }}</h4>
<p v-html="$t('commGuidePara054')"></p>
<p v-html="$t('commGuidePara055')"></p>
<ul>
<li v-html="$t('commGuideList06A')"></li>
<li v-html="$t('commGuideList06B')"></li>
<li v-html="$t('commGuideList06C')"></li>
<li v-html="$t('commGuideList06D')"></li>
<li v-html="$t('commGuideList06E')"></li>
</ul>
<h4>{{ $t('commGuideHeadingMinorInfractions') }}</h4>
<p v-html="$t('commGuidePara056')"></p>
<p v-html="$t('commGuidePara057')"></p>
<ul>
<li v-html="$t('commGuideList07A')"></li>
<li v-html="$t('commGuideList07B')"></li>
</ul>
<p v-html="$t('commGuidePara057A')"></p>
<h3 id="consequences">
{{ $t('commGuideHeadingConsequences') }}
</h3>
<p v-html="$t('commGuidePara059')"></p>
<img src="~@/assets/images/community-guidelines/consequences.png">
<div class="media align-items-center">
<div class="media-body">
<p v-html="$t('commGuidePara058')"></p>
<p v-html="$t('commGuidePara059')"></p>
</div>
<img src="~@/assets/images/community-guidelines/consequences.png">
</div>
<p v-html="$t('commGuidePara060')"></p>
<ul>
<li v-html="$t('commGuideList08A')"></li>
<li v-html="$t('commGuideList08B')"></li>
<li v-html="$t('commGuideList08C')"></li>
</ul>
<p v-html="$t('commGuidePara060A')"></p>
<p v-html="$t('commGuidePara060B')"></p>
<h4>{{ $t('commGuideHeadingSevereConsequences') }}</h4>
<ul>
<li v-html="$t('commGuideList09A')"></li>
<li v-html="$t('commGuideList09C')"></li>
<li v-html="$t('commGuideList09D')"></li>
<li v-html="$t('commGuideList09E')"></li>
</ul>
<h4>{{ $t('commGuideHeadingModerateConsequences') }}</h4>
<ul>
<li>
{{ $t('commGuideList10A') }}
<ul>
<li v-html="$t('commGuideList10A1')"></li>
</ul>
</li>
<li v-html="$t('commGuideList10D')"></li>
<li v-html="$t('commGuideList10G')"></li>
<li v-html="$t('commGuideList10F')"></li>
</ul>
<h4>{{ $t('commGuideHeadingMinorConsequences') }}</h4>
<ul>
@@ -103,53 +166,56 @@
<h3 id="restoration">
{{ $t('commGuideHeadingRestoration') }}
</h3>
<img
src="~@/assets/images/community-guidelines/restoration.png"
class="mb-3"
>
<p v-html="$t('commGuidePara061')"></p>
<div class="media align-items-center">
<img src="~@/assets/images/community-guidelines/restoration.png">
<div class="media-body">
<p v-html="$t('commGuidePara061')"></p>
<p v-html="$t('commGuidePara062')"></p>
</div>
</div>
<p v-html="$t('commGuidePara063')"></p>
<h2 id="meet-the-mods">
{{ $t('commGuideHeadingMeet') }}
</h2>
<p v-html="$t('commGuidePara007')"></p>
<p v-html="$t('commGuidePara009')"></p>
<img
src="~@/assets/images/community-guidelines/staff.png"
class="mb-3"
>
<ul>
<li>
{{ $t('commGuideAKA', {habitName: 'heyeilatan', realName: 'Natalie'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'CuriousMagpie'}) }})
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'redphoenix', realName: 'Vicky'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'veeeeeee'}) }})
- Co-Founder
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Beffymaroo', realName: 'Beth'}) }}
- Art, Community Management, Many Hats
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Sabe'}) }}
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Apollo', realName: 'Tressley'}) }}
- Designer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Piyo', realName: 'Sara'}) }}
- Mobile Designer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
- Mobile Developer
</li>
</ul>
<div class="media align-items-center">
<img src="~@/assets/images/community-guidelines/staff.png">
<div class="media-body">
<ul>
<li>
{{ $t('commGuideAKA', {habitName: 'heyeilatan', realName: 'Natalie'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'CuriousMagpie'}) }})
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
- Mobile Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'redphoenix', realName: 'Vicky'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'veeeeeee'}) }})
- Co-Founder
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Beffymaroo', realName: 'Beth'}) }}
- Art, Community Management, Many Hats
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Sabe'}) }}
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Apollo', realName: 'Tressley'}) }}
- Designer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Piyo', realName: 'Sara'}) }}
- Mobile Designer
</li>
</ul>
</div>
</div>
<p v-html="$t('commGuidePara013')"></p>
<p>
{{ $t('commGuidePara014') }}<br>
@@ -157,6 +223,7 @@
Lemoness, lefnire, Slappybag, litenull, Shaner, Bobbyroberts99, wc8,
Breadstrings, Megan, Blade, Daniel the Bard, deilann, shanaqui, Nakonana,
Dewines, Alys, Fox_town, MaybeSteveRogers, and Cantras.
</em>
</p>
<h2 id="final">
@@ -168,12 +235,16 @@
{{ $t('commGuideHeadingLinks') }}
</h2>
<ul>
<li><a href="/static/faq">{{ $t('faq') }}</a></li>
<li v-html="$t('commGuideLink01')"></li>
<li v-html="$t('commGuideLink02')"></li>
<li v-html="$t('commGuideLink03')"></li>
<li v-html="$t('commGuideLink04')"></li>
<li v-html="$t('commGuideLink06')"></li>
<li v-html="$t('commGuideLink07')"></li>
</ul>
<p v-html="$t('commGuidePara069')"></p>
<ul>
<ul class="list-2col list-unstyled">
<li>Beffymaroo</li>
<li>Breadstrings</li>
<li>Draayder</li>
<li>Kiwibot</li>
@@ -187,3 +258,12 @@
</ul>
</div>
</template>
<style lang='scss' scoped>
.list-2col {
width: 50%;
columns: 2;
-moz-columns: 2;
-webkit-columns: 2;
}
</style>

View File

@@ -35,8 +35,8 @@
&colon;&nbsp;
<a
target="_blank"
@click.prevent="openBugReportModal(true)"
> {{ $t('askQuestion') }}</a>
href="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a"
>Habitica Help guild</a>
<br>
{{ $t('businessInquiries') }}
&colon;&nbsp;

View File

@@ -94,11 +94,6 @@
}
}
.container-fluid {
position: relative;
top: 2rem;
}
@media only screen and (max-width: 768px) {
.container-fluid {
margin: auto;

View File

@@ -31,6 +31,7 @@
</div>
<div class="row">
<div class="col-md-6">
<img src="~@/assets/images/marketing/guild.png">
<h2>{{ $t('marketing2Lead1Title') }}</h2>
<p>{{ $t('marketing2Lead1') }}</p>
<img src="~@/assets/images/marketing/vice3.png">

View File

@@ -12,15 +12,12 @@
<p v-markdown="$t(`webStep${step}Text`, stepVars[step])"></p>
<hr>
</div>
<p>
<span v-html="$t('overviewQuestionsRevised')"></span>
<a
target="_blank"
@click.prevent="openBugReportModal(true)"
>
{{ $t('askQuestion') }}
</a>
</p>
<p
v-markdown="$t('overviewQuestions', {
faqUrl: '/static/faq/',
helpGuildUrl: '/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a'
})"
></p>
</div>
</div>
</div>
@@ -38,13 +35,11 @@
<script>
import markdownDirective from '@/directives/markdown';
import reportBug from '@/mixins/reportBug.js';
export default {
directives: {
markdown: markdownDirective,
},
mixins: [reportBug],
data () {
return {
stepsNum: ['1', '2', '3'],

View File

@@ -1,12 +1,11 @@
<template>
<div>
<chat-banner />
<static-header
v-if="showContentWrap"
:class="{
'home-header': ['home', 'front'].indexOf($route.name) !== -1,
'white-header': this.$route.name === 'plans'
}"
v-if="showContentWrap"
:class="{
'home-header': ['home', 'front'].indexOf($route.name) !== -1,
'white-header': this.$route.name === 'plans'
}"
/>
<div class="static-wrapper">
<router-view />
@@ -244,13 +243,11 @@
<script>
import AppFooter from '@/components/appFooter';
import ChatBanner from '@/components/header/banners/chatBanner';
import StaticHeader from './header.vue';
export default {
components: {
AppFooter,
ChatBanner,
StaticHeader,
},
computed: {

View File

@@ -521,12 +521,7 @@ export default {
// Get Category Filter Labels
this.typeFilters = getFilterLabels(this.type, this.challenge);
// Set default filter for task column
if (this.challenge) {
this.activateFilter(this.type);
} else {
this.activateFilter(this.type, this.user.preferences.tasks.activeFilter[this.type], true);
}
this.activateFilter(this.type);
},
mounted () {
this.setColumnBackgroundVisibility();
@@ -666,7 +661,7 @@ export default {
taskSummary (task) {
this.$emit('taskSummary', task);
},
activateFilter (type, filter = '', skipSave = false) {
activateFilter (type, filter = '') {
// Needs a separate API call as this data may not reside in store
if (type === 'todo' && filter === 'complete2') {
if (this.group && this.group._id) {
@@ -682,16 +677,14 @@ export default {
// as default filter for daily
// and set the filter as 'due' only when the component first
// loads and not on subsequent reloads.
if (type === 'daily' && filter === '' && !this.challenge) {
if (
type === 'daily' && filter === '' && !this.challenge
&& this.user.preferences.dailyDueDefaultView
) {
filter = 'due'; // eslint-disable-line no-param-reassign
}
this.activeFilter = getActiveFilter(type, filter, this.challenge);
if (!skipSave && !this.challenge) {
const propertyToUpdate = `preferences.tasks.activeFilter.${type}`;
this.$store.dispatch('user:set', { [propertyToUpdate]: filter });
}
},
setColumnBackgroundVisibility () {
this.$nextTick(() => {

Some files were not shown because too many files have changed in this diff Show More