mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-13 04:37:36 +01:00
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
484c3cbac8 | ||
|
|
c199beaf8c | ||
|
|
553aa01c25 | ||
|
|
8d1b10e458 | ||
|
|
0eaee9b1e4 | ||
|
|
41bbc475ab | ||
|
|
d6e03c765e | ||
|
|
dd6503d5ef | ||
|
|
36e5f39d7c | ||
|
|
d48e4a664f | ||
|
|
661b30e807 | ||
|
|
026e819271 | ||
|
|
1fab19acf4 | ||
|
|
5743fb86b0 | ||
|
|
5443bf2459 | ||
|
|
c0d5566417 | ||
|
|
ded71b46c5 | ||
|
|
9693ad321c | ||
|
|
dd3679f329 | ||
|
|
f3029953dc | ||
|
|
01881b2fd8 | ||
|
|
11a22d0f5d | ||
|
|
5f9bf07045 | ||
|
|
719c03e2f5 | ||
|
|
379afa9554 | ||
|
|
dbc23e89b8 | ||
|
|
0c6e254742 | ||
|
|
8327e69bdd | ||
|
|
2d953f4f59 | ||
|
|
7118d63949 | ||
|
|
20af8d038e | ||
|
|
3d9dfbb5e1 | ||
|
|
ae0b966f45 | ||
|
|
cef8a34c06 | ||
|
|
6432823eec | ||
|
|
563b780d85 | ||
|
|
aa9b1b2cac | ||
|
|
401e541b86 | ||
|
|
c13bed3bad | ||
|
|
b3c4817fb4 | ||
|
|
7c9c45ac5f | ||
|
|
95142e3684 | ||
|
|
dc1cce6ddb | ||
|
|
43cf77f33c | ||
|
|
93780d7056 | ||
|
|
2ad17d408e | ||
|
|
b0f7567367 | ||
|
|
3f2b1d3f79 | ||
|
|
29eb8ca10b | ||
|
|
8c71ca12b8 | ||
|
|
72a753626f | ||
|
|
35ebb12bf2 | ||
|
|
1ff418f62d | ||
|
|
e1aa437ea5 | ||
|
|
2a4239bf3c | ||
|
|
399563435b | ||
|
|
59f7e25c85 | ||
|
|
ad845dff43 | ||
|
|
fd1eb2d900 | ||
|
|
26cb6df9d9 | ||
|
|
b0aafb079a | ||
|
|
58f0837c50 | ||
|
|
a6378b3d43 | ||
|
|
ddbf95da92 | ||
|
|
4d31e0286b | ||
|
|
7a74825121 | ||
|
|
be0e8779d5 | ||
|
|
fffbe17bcc | ||
|
|
ca4ee8b513 | ||
|
|
30f1820a49 | ||
|
|
3bb6c391af | ||
|
|
a0383c785a | ||
|
|
99790c05f4 | ||
|
|
fc5fec9bfe | ||
|
|
9db5d4116d | ||
|
|
6676e94ef6 | ||
|
|
723adceb25 | ||
|
|
440d06da4a | ||
|
|
0ea84668a8 | ||
|
|
5893d8b9bb | ||
|
|
2c799b9c07 | ||
|
|
1550d9b4ee | ||
|
|
ade812b86d | ||
|
|
62e6fbef61 | ||
|
|
67a0f8b65a | ||
|
|
aa432022d3 | ||
|
|
86fb3c1fd1 | ||
|
|
ff2b4add8b | ||
|
|
4ba73dfbec | ||
|
|
e675ea9bd1 | ||
|
|
9c27d86ced | ||
|
|
58ee81adfc | ||
|
|
32c9904a6e | ||
|
|
b86e0a1549 | ||
|
|
154ac9bb38 | ||
|
|
a97060445a | ||
|
|
26b59de1de | ||
|
|
21c8b00ef6 |
@@ -7,5 +7,14 @@ module.exports = {
|
||||
rules: {
|
||||
'prefer-regex-literals': 'warn',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'require-await': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['migrations/**', 'gulp/**'], // Or *.test.js
|
||||
rules: {
|
||||
'require-await': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@@ -1,6 +1,13 @@
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'phillip/**'
|
||||
- 'sabrecat/**'
|
||||
- 'kalista/**'
|
||||
- 'natalie/**'
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -47,5 +47,5 @@ webpack.webstorm.config
|
||||
|
||||
# mongodb replica set for local dev
|
||||
mongodb-*.tgz
|
||||
/mongodb-data
|
||||
/mongodb-data*
|
||||
/.nyc_output
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
This webpage includes the documentation for version 3 of the [Habitica](https://habitica.com) API.
|
||||
|
||||
If you're developing a 3rd party tool that uses the Habitica API you should read the [Guidance for Comrades](https://habitica.fandom.com/wiki/Guidance_for_Comrades) and in particular the section called [Rules for Third-Party Tools](https://habitica.fandom.com/wiki/Guidance_for_Comrades#Rules_for_Third-Party_Tools) which includes suggestions on how to best use the API and the rules to follow when interacting with it.
|
||||
If you're developing a 3rd party tool that uses the Habitica API, read the [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines), which describe how to be a responsible user of our server resources!
|
||||
|
||||
@@ -93,5 +93,6 @@
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"TIME_TRAVEL_ENABLED": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||
"SLOW_REQUEST_THRESHOLD": 1000
|
||||
}
|
||||
|
||||
@@ -64,6 +64,15 @@ function filterFile (file) {
|
||||
if (file.relative.indexOf('icon_background') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('notif_') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('quest_') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('inventory_quest_') === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Submodule habitica-images updated: dedbcf0f24...142936a93a
@@ -26,7 +26,7 @@ async function updateUser (user) {
|
||||
[{ name: 'BASE_URL', content: BASE_URL }], // Add variables from template
|
||||
);
|
||||
|
||||
return User.update({ _id: user._id }, { $set: { migration: MIGRATION_NAME } }).exec();
|
||||
return User.updateOne({ _id: user._id }, { $set: { migration: MIGRATION_NAME } }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
|
||||
@@ -27,13 +27,13 @@ async function updateUser (user) {
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
||||
return User.updateOne({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.local.lowerCaseUsername': 'olson1',
|
||||
'auth.local.username': 'ExampleHabitican',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
@@ -57,7 +57,7 @@ async function updateUser (user) {
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.local.username': 'SabreTest',
|
||||
'auth.local.username': 'ExampleHabitican',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
125
migrations/users/nye.js
Normal file
125
migrations/users/nye.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/* eslint-disable no-console */
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20231228_nye';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const updateOp = {
|
||||
$set: { migration: MIGRATION_NAME },
|
||||
$push: { },
|
||||
};
|
||||
const data = {
|
||||
title: 'Happy New Year!',
|
||||
destination: '/inventory/equipment',
|
||||
};
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_nye2023 !== 'undefined') {
|
||||
updateOp.$inc = {
|
||||
'items.food.Candy_Skeleton': 1,
|
||||
'items.food.Candy_Base': 1,
|
||||
'items.food.Candy_CottonCandyBlue': 1,
|
||||
'items.food.Candy_CottonCandyPink': 1,
|
||||
'items.food.Candy_Shade': 1,
|
||||
'items.food.Candy_White': 1,
|
||||
'items.food.Candy_Golden': 1,
|
||||
'items.food.Candy_Zombie': 1,
|
||||
'items.food.Candy_Desert': 1,
|
||||
'items.food.Candy_Red': 1,
|
||||
};
|
||||
data.icon = 'notif_candy_nye';
|
||||
data.text = 'You’ve received an assortment of candy to celebrate with your Pets!';
|
||||
data.destination = '/inventory/stable';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2022 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2023'] = true;
|
||||
data.icon = 'notif_2023hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Ludicrous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2022'] = true;
|
||||
data.icon = 'notif_2022hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Fabulous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2021'] = true;
|
||||
data.icon = 'notif_2021hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Preposterous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2020'] = true;
|
||||
data.icon = 'notif_2020hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Extravagant Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2019'] = true;
|
||||
data.icon = 'notif_2019hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Outrageous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2018'] = true;
|
||||
data.icon = 'notif_2018hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Outlandish Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2017'] = true;
|
||||
data.icon = 'notif_2017hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Fanciful Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2016'] = true;
|
||||
data.icon = 'notif_2016hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Whimsical Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2015'] = true;
|
||||
data.icon = 'notif_2015hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Ridiculous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2014'] = true;
|
||||
data.icon = 'notif_2014hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Silly Party Hat!';
|
||||
} else {
|
||||
updateOp.$set['items.gear.owned.head_special_nye'] = true;
|
||||
data.icon = 'notif_2013hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Absurd Party Hat!';
|
||||
}
|
||||
|
||||
updateOp.$push.notifications = {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data,
|
||||
seen: false,
|
||||
};
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.updateOne({ _id: user._id }, updateOp).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2023-12-01') },
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({ _id: 1 })
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
}
|
||||
461
package-lock.json
generated
461
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "5.32.0",
|
||||
"version": "5.36.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habitica",
|
||||
"version": "5.32.0",
|
||||
"version": "5.36.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -22,7 +22,7 @@
|
||||
"apple-auth": "^1.0.9",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^2.0.0",
|
||||
@@ -33,7 +33,7 @@
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-habitrpg": "^6.2.3",
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
"express": "^4.19.2",
|
||||
"express": "^4.21.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-validator": "^5.2.0",
|
||||
"firebase-admin": "^12.1.1",
|
||||
@@ -56,7 +56,7 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^7.6.3",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
@@ -3044,10 +3044,10 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz",
|
||||
"integrity": "sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==",
|
||||
"optional": true,
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
|
||||
"integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
@@ -3677,14 +3677,15 @@
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
|
||||
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
|
||||
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
@@ -6244,9 +6245,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
@@ -6256,7 +6257,7 @@
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
@@ -6291,11 +6292,11 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/body-parser/node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -6401,10 +6402,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz",
|
||||
"integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==",
|
||||
"dev": true,
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz",
|
||||
"integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
}
|
||||
@@ -6577,14 +6578,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz",
|
||||
"integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.3",
|
||||
"set-function-length": "^1.2.0"
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -7369,9 +7371,9 @@
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -8376,9 +8378,9 @@
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -8497,6 +8499,17 @@
|
||||
"resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
|
||||
"integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
@@ -9980,36 +9993,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
@@ -10062,11 +10075,11 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/express/node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -10531,12 +10544,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
@@ -13348,11 +13361,6 @@
|
||||
"resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
|
||||
"integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA=="
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -14267,9 +14275,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/kareem": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
|
||||
"integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==",
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
|
||||
"integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
@@ -14278,7 +14287,7 @@
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/kerberos/-/kerberos-1.1.7.tgz",
|
||||
"integrity": "sha512-1zXg4rARjsh/VMz2jjZeTfRHbJTVNR6f2DYHbLvtUSOW1satj33Fvc7vOJ0YVWB9+/9ITJWd1QKp4w217SsiFA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
@@ -14936,8 +14945,7 @@
|
||||
"node_modules/memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||
"optional": true
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
|
||||
},
|
||||
"node_modules/meow": {
|
||||
"version": "3.7.0",
|
||||
@@ -14961,9 +14969,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
@@ -15443,43 +15454,47 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
|
||||
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^8.2.1",
|
||||
"whatwg-url": "^11.0.0"
|
||||
"@types/whatwg-url": "^11.0.2",
|
||||
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
|
||||
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.1"
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
|
||||
"integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^3.0.0",
|
||||
"tr46": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-core": {
|
||||
@@ -15564,55 +15579,64 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "7.6.8",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.6.8.tgz",
|
||||
"integrity": "sha512-q9zAySH+UtOK5yonWyNcLfq3PxrY6s4gdta4qNGKNOE2yTVoY9FP4hQtvWYnv4rkdk7T8QmQMC7bbhJjDxIunw==",
|
||||
"version": "8.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.7.tgz",
|
||||
"integrity": "sha512-mvNXmU0V8qZzMR/qoK2mjT4Ti2ALdtfS0teK+twxhlGkwzOD76V02/zWajTu2MJ7QyEmZe9OWvnJsIY0iAuX3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^5.5.0",
|
||||
"kareem": "2.5.1",
|
||||
"mongodb": "5.9.1",
|
||||
"bson": "^6.10.1",
|
||||
"kareem": "2.6.3",
|
||||
"mongodb": "~6.12.0",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "5.0.0",
|
||||
"ms": "2.1.3",
|
||||
"sift": "16.0.1"
|
||||
"sift": "17.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/bson": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
|
||||
"integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==",
|
||||
"node_modules/mongoose/node_modules/kerberos": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.2.1.tgz",
|
||||
"integrity": "sha512-Vlyv1tjAPb0y2VIJ03dKkUjsneGIBuTkH24uGRx6/DrKpFlVuGPmct3m5aEotljVUlw7PAGWABwR5aNeW7y8Zw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^6.1.0",
|
||||
"prebuild-install": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
"node": ">=12.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/mongodb": {
|
||||
"version": "5.9.1",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.1.tgz",
|
||||
"integrity": "sha512-NBGA8AfJxGPeB12F73xXwozt8ZpeIPmCUeWRwl9xejozTXFes/3zaep9zhzs1B/nKKsw4P3I4iPfXl3K7s6g+Q==",
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
|
||||
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bson": "^5.5.0",
|
||||
"mongodb-connection-string-url": "^2.6.0",
|
||||
"socks": "^2.7.1"
|
||||
"@mongodb-js/saslprep": "^1.1.9",
|
||||
"bson": "^6.10.1",
|
||||
"mongodb-connection-string-url": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.0"
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.188.0",
|
||||
"@mongodb-js/zstd": "^1.0.0",
|
||||
"kerberos": "^1.0.0 || ^2.0.0",
|
||||
"mongodb-client-encryption": ">=2.3.0 <3",
|
||||
"snappy": "^7.2.2"
|
||||
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
||||
"gcp-metadata": "^5.2.0",
|
||||
"kerberos": "^2.0.1",
|
||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||
"snappy": "^7.2.2",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
@@ -15621,6 +15645,9 @@
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -15629,6 +15656,9 @@
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
},
|
||||
"socks": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15637,6 +15667,105 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/mongoose/node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mongoose/node_modules/node-abi": {
|
||||
"version": "3.74.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
|
||||
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/node-addon-api": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mongoose/node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monk": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/monk/-/monk-7.3.4.tgz",
|
||||
@@ -16050,7 +16179,7 @@
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz",
|
||||
"integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"semver": "^5.4.1"
|
||||
}
|
||||
@@ -16059,7 +16188,7 @@
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
@@ -16249,7 +16378,7 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
|
||||
"integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "1.0.10",
|
||||
@@ -17592,9 +17721,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "1.1.0",
|
||||
@@ -17864,7 +17993,7 @@
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.2.tgz",
|
||||
"integrity": "sha512-PzYWIKZeP+967WuKYXlTOhYBgGOvTRSfaKI89XnfJ0ansRAH7hDU45X+K+FZeI1Wb/7p/NnuctPH3g0IqKUuSQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"expand-template": "^2.0.3",
|
||||
@@ -17892,7 +18021,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -17901,13 +18030,13 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/are-we-there-yet": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
|
||||
"integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^2.0.6"
|
||||
@@ -17917,7 +18046,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
@@ -17929,7 +18058,7 @@
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
"integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3",
|
||||
"console-control-strings": "^1.0.0",
|
||||
@@ -17945,7 +18074,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
},
|
||||
@@ -17957,13 +18086,13 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/npmlog": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "~1.1.2",
|
||||
"console-control-strings": "~1.1.0",
|
||||
@@ -17975,7 +18104,7 @@
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -17990,7 +18119,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -17999,7 +18128,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -18013,7 +18142,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
},
|
||||
@@ -19337,9 +19466,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -19372,6 +19501,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -19386,14 +19523,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -19519,11 +19656,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
|
||||
"integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.6",
|
||||
"call-bind": "^1.0.7",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"object-inspect": "^1.13.1"
|
||||
@@ -19536,9 +19673,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sift": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
|
||||
"integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ=="
|
||||
"version": "17.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
|
||||
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
@@ -19568,7 +19706,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
|
||||
"integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^4.2.0",
|
||||
"once": "^1.3.1",
|
||||
@@ -19579,7 +19717,7 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
|
||||
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^2.0.0"
|
||||
},
|
||||
@@ -19591,7 +19729,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
||||
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
@@ -19732,15 +19870,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
@@ -19860,19 +19989,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
|
||||
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
|
||||
"dependencies": {
|
||||
"ip": "^2.0.0",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sort-keys": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
|
||||
@@ -19950,7 +20066,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.32.0",
|
||||
"version": "5.36.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -17,7 +17,7 @@
|
||||
"apple-auth": "^1.0.9",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^2.0.0",
|
||||
@@ -28,7 +28,7 @@
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-habitrpg": "^6.2.3",
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
"express": "^4.19.2",
|
||||
"express": "^4.21.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-validator": "^5.2.0",
|
||||
"firebase-admin": "^12.1.1",
|
||||
@@ -51,7 +51,7 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^7.6.3",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
@@ -110,6 +110,7 @@
|
||||
"start:simple": "node ./website/server/index.js",
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { recoverCron, cron } from '../../../../website/server/libs/cron';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateTodo,
|
||||
generateDaily,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import common from '../../../../website/common';
|
||||
import * as analytics from '../../../../website/server/libs/analyticsService';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
|
||||
// const scoreTask = common.ops.scoreTask;
|
||||
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
|
||||
const pathToCronLib = '../../../../website/server/libs/cron';
|
||||
|
||||
@@ -1200,7 +1209,7 @@ describe('cron', async () => {
|
||||
it('increments perfect day achievement if all (at least 1) due dailies were completed', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
@@ -1212,7 +1221,7 @@ describe('cron', async () => {
|
||||
it('does not increment perfect day achievement if no due dailies', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
@@ -1224,7 +1233,7 @@ describe('cron', async () => {
|
||||
it('gives perfect day buff if all (at least 1) due dailies were completed', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
@@ -1242,7 +1251,7 @@ describe('cron', async () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
@@ -1259,7 +1268,7 @@ describe('cron', async () => {
|
||||
it('clears buffs if user does not have a perfect day (no due dailys)', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
@@ -1488,78 +1497,6 @@ describe('cron', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifications', async () => {
|
||||
it('adds a user notification', async () => {
|
||||
const mpBefore = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
|
||||
daysMissed = 1;
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore,
|
||||
mp: user.stats.mp - mpBefore,
|
||||
});
|
||||
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
|
||||
it('condenses multiple notifications into one', async () => {
|
||||
const mpBefore1 = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
|
||||
daysMissed = 1;
|
||||
const hpBefore1 = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore1,
|
||||
mp: user.stats.mp - mpBefore1,
|
||||
});
|
||||
|
||||
const notifsBefore2 = user.notifications.length;
|
||||
const hpBefore2 = user.stats.hp;
|
||||
const mpBefore2 = user.stats.mp;
|
||||
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length - notifsBefore2).to.equal(0);
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1),
|
||||
mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1),
|
||||
});
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('private messages', async () => {
|
||||
let lastMessageId;
|
||||
|
||||
@@ -1606,7 +1543,7 @@ describe('cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.notifications.length).to.be.greaterThan(1);
|
||||
expect(user.notifications.length).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
@@ -1820,64 +1757,258 @@ describe('cron', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('recoverCron', async () => {
|
||||
let locals; let status; let
|
||||
execStub;
|
||||
describe('cron wrapper', () => {
|
||||
let res; let
|
||||
req;
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
execStub = sandbox.stub();
|
||||
sandbox.stub(User, 'findOne').returns({ exec: execStub });
|
||||
|
||||
status = { times: 0 };
|
||||
locals = {
|
||||
user: new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'email@example.com',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analytics;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('throws an error if user cannot be found', async () => {
|
||||
execStub.returns(Promise.resolve(null));
|
||||
it('calls next when user is not attached', async () => {
|
||||
res.locals.user = null;
|
||||
await cronWrapper(req, res);
|
||||
});
|
||||
|
||||
it('calls next when days have not been missed', async () => {
|
||||
await cronWrapper(req, res);
|
||||
});
|
||||
|
||||
it('should clear todos older than 30 days for free users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.not.exist;
|
||||
});
|
||||
|
||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await Promise.all([task.save(), user.save()]);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.exist;
|
||||
});
|
||||
|
||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.not.exist;
|
||||
});
|
||||
|
||||
it('should call next if user was not modified after cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
});
|
||||
|
||||
it('runs cron if previous cron was incomplete', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
});
|
||||
|
||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
});
|
||||
|
||||
it('does damage for missing dailies', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const updatedUser = await User.findOne({ _id: user._id });
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
|
||||
it('updates tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
await Promise.all([todo.save(), user.save()]);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
});
|
||||
|
||||
it('updates large number of tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
const start = new Date();
|
||||
const saves = [todo.save(), user.save()];
|
||||
for (let i = 0; i < 200; i += 1) {
|
||||
const newTodo = generateTodo(user);
|
||||
newTodo.value = i;
|
||||
saves.push(newTodo.save());
|
||||
}
|
||||
await Promise.all(saves);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const duration = new Date() - start;
|
||||
expect(duration).to.be.lessThan(1000);
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(moment(start).isSame(user.lastCron, 'day'));
|
||||
expect(moment(start).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
});
|
||||
|
||||
it('fails entire cron if one task is failing', async () => {
|
||||
const lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
user.lastCron = lastCron;
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
const badTodo = generateTodo(user);
|
||||
badTodo.text = 'bad todo';
|
||||
badTodo.attribute = 'bad';
|
||||
await Promise.all([badTodo.save({ validateBeforeSave: false }), todo.save(), user.save()]);
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when user cannot be found');
|
||||
await cronWrapper(req, res);
|
||||
} catch (err) {
|
||||
expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`);
|
||||
expect(err).to.exist;
|
||||
}
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(moment(lastCron).isSame(user.lastCron, 'day'));
|
||||
expect(todoFound.value).to.be.equal(todoValueBefore);
|
||||
});
|
||||
|
||||
it('applies quest progress', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
|
||||
const questKey = 'dilatory';
|
||||
user.party.quest.key = questKey;
|
||||
|
||||
const party = new Group({
|
||||
type: 'party',
|
||||
name: generateUUID(),
|
||||
leader: user._id,
|
||||
});
|
||||
party.quest.members[user._id] = true;
|
||||
party.quest.key = questKey;
|
||||
await party.save();
|
||||
|
||||
user.party._id = party._id;
|
||||
await user.save();
|
||||
|
||||
party.startQuest(user);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const updatedUser = await User.findOne({ _id: user._id });
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
|
||||
it('cronSignature less than 5 minutes ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
try {
|
||||
await cronWrapper(req, res);
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
}
|
||||
});
|
||||
|
||||
it('increases status.times count and reruns up to 4 times', async () => {
|
||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
||||
execStub.onCall(4).returns(Promise.resolve({ _cronSignature: 'NOT_RUNNING' }));
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await recoverCron(status, locals);
|
||||
|
||||
expect(status.times).to.eql(4);
|
||||
expect(locals.user).to.eql({ _cronSignature: 'NOT_RUNNING' });
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
});
|
||||
|
||||
it('throws an error if recoverCron runs 5 times', async () => {
|
||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
||||
it('cron should not run more than once', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
const result = await Promise.allSettled([
|
||||
cronWrapper(req, res),
|
||||
cronWrapper(req, res),
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when recoverCron runs 5 times');
|
||||
} catch (err) {
|
||||
expect(status.times).to.eql(5);
|
||||
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
|
||||
const runResult = await cronWrapper(req, res);
|
||||
if (runResult !== null) {
|
||||
reject(new Error('cron ran more than once'));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}, 200);
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(result.filter(r => r.status === 'fulfilled')).to.have.lengthOf(2);
|
||||
expect(result.filter(r => r.status === 'rejected')).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,23 +171,23 @@ describe('emails', () => {
|
||||
expect(got.post).not.to.be.called;
|
||||
});
|
||||
|
||||
it('throws error when mail target is only a string', () => {
|
||||
it('throws error when mail target is only a string', async () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = 'my email';
|
||||
|
||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
||||
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||
});
|
||||
|
||||
it('throws error when mail target has no _id or email', () => {
|
||||
it('throws error when mail target has no _id or email', async () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
|
||||
};
|
||||
|
||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
||||
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||
});
|
||||
|
||||
it('throws error when variables not an array', () => {
|
||||
it('throws error when variables not an array', async () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
name: 'my name',
|
||||
@@ -195,9 +195,10 @@ describe('emails', () => {
|
||||
};
|
||||
const variables = {};
|
||||
|
||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
||||
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: is not an array');
|
||||
});
|
||||
it('throws error when variables array not contain name/content', () => {
|
||||
|
||||
it('throws error when variables array not contain name/content', async () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
name: 'my name',
|
||||
@@ -209,8 +210,9 @@ describe('emails', () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
||||
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: does not contain name or content');
|
||||
});
|
||||
|
||||
it('throws no error when variables array contain name but no content', () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
|
||||
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
||||
@@ -29,22 +28,4 @@ describe('mongodb', () => {
|
||||
expect(string).to.equal('mongodb://hostname:3030');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultConnectionOptions', () => {
|
||||
it('returns development config when IS_PROD is false', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
||||
});
|
||||
|
||||
it('returns production config when IS_PROD is true', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateTodo,
|
||||
generateDaily,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import cronMiddleware from '../../../../website/server/middlewares/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
import * as cronLib from '../../../../website/server/libs/cron';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
|
||||
describe('cron middleware', () => {
|
||||
let res; let
|
||||
req;
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analyticsService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('calls next when user is not attached', done => {
|
||||
res.locals.user = null;
|
||||
cronMiddleware(req, res, done);
|
||||
});
|
||||
|
||||
it('calls next when days have not been missed', done => {
|
||||
cronMiddleware(req, res, done);
|
||||
});
|
||||
|
||||
it('should clear todos older than 30 days for free users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.not.exist;
|
||||
resolve();
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.exist;
|
||||
return resolve();
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.not.exist;
|
||||
return resolve();
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next if user was not modified after cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('runs cron if previous cron was incomplete', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does damage for missing dailies', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
await Promise.all([todo.save(), user.save()]);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('applies quest progress', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
|
||||
const questKey = 'dilatory';
|
||||
user.party.quest.key = questKey;
|
||||
|
||||
const party = new Group({
|
||||
type: 'party',
|
||||
name: generateUUID(),
|
||||
leader: user._id,
|
||||
});
|
||||
party.quest.members[user._id] = true;
|
||||
party.quest.key = questKey;
|
||||
await party.save();
|
||||
|
||||
user.party._id = party._id;
|
||||
await user.save();
|
||||
|
||||
party.startQuest(user);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('recovers from failed cron and does not error when user is already cronning', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
const updatedUser = user.toObject();
|
||||
updatedUser.matchedCount = 0;
|
||||
|
||||
sandbox.spy(cronLib, 'recoverCron');
|
||||
|
||||
sandbox.stub(User, 'updateOne')
|
||||
.withArgs({
|
||||
_id: user._id,
|
||||
$or: [
|
||||
{ _cronSignature: 'NOT_RUNNING' },
|
||||
{ _cronSignature: { $lt: sinon.match.number } },
|
||||
],
|
||||
})
|
||||
.returns({
|
||||
exec () {
|
||||
return Promise.resolve(updatedUser);
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(cronLib.recoverCron).to.be.calledOnce;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature less than an hour ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (!err) return reject(new Error('Cron should have failed.'));
|
||||
expect(err.message).to.be.equal(expectedErrMessage);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cron should not run more than once', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
sandbox.spy(cronLib, 'cron');
|
||||
|
||||
await Promise.all([new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}), new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}), new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}, 400);
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(cronLib.cron).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ describe('POST /debug/jump-time', () => {
|
||||
expect(resultDate.getDate()).to.eql(today.getDate());
|
||||
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 355 })).time);
|
||||
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 365 })).time);
|
||||
expect(newResultDate.getFullYear()).to.eql(today.getFullYear() + 1);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('GET /heroes/:heroId', () => {
|
||||
const heroFields = [
|
||||
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
|
||||
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
|
||||
'stats',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ describe('PUT /heroes/:heroId', () => {
|
||||
const heroFields = [
|
||||
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
|
||||
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
|
||||
'stats',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
@@ -60,12 +61,12 @@ describe('PUT /heroes/:heroId', () => {
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
// test response values
|
||||
expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(heroRes.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||
expect(heroRes.contributor.level).to.equal(1);
|
||||
expect(heroRes.purchased.ads).to.equal(true);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(hero.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||
expect(hero.contributor.level).to.equal(1);
|
||||
expect(hero.purchased.ads).to.equal(true);
|
||||
expect(hero.auth.blocked).to.equal(prevBlockState);
|
||||
@@ -136,12 +137,12 @@ describe('PUT /heroes/:heroId', () => {
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
// test response values
|
||||
expect(heroRes.balance).to.equal(1); // 0+1 for sixth contrib level
|
||||
expect(heroRes.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||
expect(heroRes.contributor.level).to.equal(6);
|
||||
expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.balance).to.equal(1); // 0+1 for sixth contrib level
|
||||
expect(hero.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||
expect(hero.contributor.level).to.equal(6);
|
||||
expect(hero.items.pets['Dragon-Hydra']).to.equal(5);
|
||||
});
|
||||
|
||||
56
test/api/v3/integration/members/GET-members_username.test.js
Normal file
56
test/api/v3/integration/members/GET-members_username.test.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import common from '../../../../../website/common';
|
||||
|
||||
describe('GET /members/username/:username', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.username', async () => {
|
||||
await expect(user.get('/members/username/')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a member\'s public data only', async () => {
|
||||
// make sure user has all the fields that can be returned by the getMember call
|
||||
const member = await generateUser({
|
||||
contributor: { level: 1 },
|
||||
backer: { tier: 3 },
|
||||
preferences: {
|
||||
costume: false,
|
||||
background: 'volcano',
|
||||
},
|
||||
secret: {
|
||||
text: 'Clark Kent',
|
||||
},
|
||||
});
|
||||
const memberRes = await user.get(`/members/username/${member.auth.local.username}`);
|
||||
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exist;
|
||||
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
|
||||
expect(memberRes.inbox.optOut).to.exist;
|
||||
expect(memberRes.inbox.canReceive).to.exist;
|
||||
expect(memberRes.inbox.messages).to.not.exist;
|
||||
expect(memberRes.secret).to.not.exist;
|
||||
|
||||
expect(memberRes.blocks).to.not.exist;
|
||||
});
|
||||
});
|
||||
@@ -101,34 +101,6 @@ describe('GET /tasks/user', () => {
|
||||
expect(allCompletedTodos[allCompletedTodos.length - 1].text).to.equal('todo to complete 2');
|
||||
});
|
||||
|
||||
it('returns only some completed todos if req.query.type is "completedTodos" or "_allCompletedTodos"', async () => {
|
||||
const LIMIT = 30;
|
||||
const numberOfTodos = LIMIT + 1;
|
||||
const todosInput = [];
|
||||
|
||||
for (let i = 0; i < numberOfTodos; i += 1) {
|
||||
todosInput[i] = { text: `todo to complete ${i}`, type: 'todo' };
|
||||
}
|
||||
const todos = await user.post('/tasks/user', todosInput);
|
||||
await user.sync();
|
||||
const initialTodoCount = user.tasksOrder.todos.length;
|
||||
|
||||
for (let i = 0; i < numberOfTodos; i += 1) {
|
||||
const id = todos[i]._id;
|
||||
|
||||
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
await user.sync();
|
||||
|
||||
expect(user.tasksOrder.todos.length).to.equal(initialTodoCount - numberOfTodos);
|
||||
|
||||
const completedTodos = await user.get('/tasks/user?type=completedTodos');
|
||||
expect(completedTodos.length).to.equal(LIMIT);
|
||||
|
||||
const allCompletedTodos = await user.get('/tasks/user?type=_allCompletedTodos');
|
||||
expect(allCompletedTodos.length).to.equal(numberOfTodos);
|
||||
});
|
||||
|
||||
it('returns dailies with isDue for the date specified', async () => {
|
||||
// @TODO Add required format
|
||||
const startDate = moment().subtract('1', 'days').toISOString();
|
||||
|
||||
104
test/api/v4/inbox/POST-inbox_message_like.test.js
Normal file
104
test/api/v4/inbox/POST-inbox_message_like.test.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import find from 'lodash/find';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
/**
|
||||
* Checks the messages array if the uniqueMessageId has the like flag
|
||||
* @param {InboxMessage[]} messages
|
||||
* @param {String} uniqueMessageId
|
||||
* @param {String} userId
|
||||
* @param {Boolean} likeStatus
|
||||
*/
|
||||
function expectMessagesLikeStatus (messages, uniqueMessageId, userId, likeStatus) {
|
||||
const messageToCheck = find(messages, { uniqueMessageId });
|
||||
|
||||
expect(messageToCheck.likes[userId]).to.equal(likeStatus);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line mocha/no-exclusive-tests
|
||||
describe('POST /inbox/like-private-message/:messageId', () => {
|
||||
let userToSendMessage;
|
||||
const getLikeUrl = messageId => `/inbox/like-private-message/${messageId}`;
|
||||
|
||||
before(async () => {
|
||||
userToSendMessage = await generateUser();
|
||||
});
|
||||
|
||||
it('returns an error when private message is not found', async () => {
|
||||
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageGroupChatNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('likes a message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: 'some message :)',
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const { uniqueMessageId } = sentMessageResult.message;
|
||||
|
||||
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||
|
||||
const senderMessages = await userToSendMessage.get('/inbox/messages');
|
||||
|
||||
expectMessagesLikeStatus(senderMessages, uniqueMessageId, receiver._id, true);
|
||||
|
||||
const receiversMessages = await receiver.get('/inbox/messages');
|
||||
|
||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
|
||||
});
|
||||
|
||||
it('allows a user to like their own private message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: 'some message :)',
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const { uniqueMessageId } = sentMessageResult.message;
|
||||
|
||||
const likeResult = await userToSendMessage.post(getLikeUrl(uniqueMessageId));
|
||||
expect(likeResult.likes[userToSendMessage._id]).to.equal(true);
|
||||
|
||||
const messages = await userToSendMessage.get('/inbox/messages');
|
||||
expectMessagesLikeStatus(messages, uniqueMessageId, userToSendMessage._id, true);
|
||||
|
||||
const receiversMessages = await receiver.get('/inbox/messages');
|
||||
|
||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
|
||||
});
|
||||
|
||||
it('unlikes a message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: 'some message :)',
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const { uniqueMessageId } = sentMessageResult.message;
|
||||
|
||||
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||
|
||||
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||
|
||||
const unlikeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||
|
||||
expect(unlikeResult.likes[receiver._id]).to.equal(false);
|
||||
|
||||
const messages = await userToSendMessage.get('/inbox/messages');
|
||||
|
||||
const messageToCheck = find(messages, { id: sentMessageResult.message.id });
|
||||
expect(messageToCheck.likes[receiver._id]).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ describe('events', () => {
|
||||
});
|
||||
|
||||
it('returns empty array when no events are active', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-11'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.be.empty;
|
||||
});
|
||||
|
||||
@@ -144,6 +144,12 @@ describe('Content Schedule', () => {
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
|
||||
it('sets the end date in new year for a winter gala', () => {
|
||||
const date = new Date('2025-01-04');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
|
||||
it('uses correct date for first hours of the month', () => {
|
||||
// if the date is checked before CONTENT_SWITCHOVER_TIME_OFFSET,
|
||||
// it should be considered the previous month
|
||||
@@ -184,7 +190,7 @@ describe('Content Schedule', () => {
|
||||
const date = new Date('2024-04-15');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.premiumHatchingPotions).to.exist;
|
||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
|
||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(6);
|
||||
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
|
||||
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
|
||||
});
|
||||
@@ -266,6 +272,21 @@ describe('Content Schedule', () => {
|
||||
expect(matcher.match('backgroundkey072024')).to.be.true;
|
||||
});
|
||||
|
||||
it('allows background matching the month for new backgrounds from multiple years', () => {
|
||||
const date = new Date('2026-07-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
expect(matcher.match('backgroundkey072024')).to.be.true;
|
||||
expect(matcher.match('backgroundkey072025')).to.be.true;
|
||||
expect(matcher.match('backgroundkey072026')).to.be.true;
|
||||
});
|
||||
|
||||
it('allows background matching the previous month in the first week for new backgrounds', () => {
|
||||
const date = new Date('2024-09-02');
|
||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
expect(matcher.match('backgroundkey082024')).to.be.true;
|
||||
expect(matcher.match('backgroundkey092024')).to.be.false;
|
||||
});
|
||||
|
||||
it('disallows background in the future', () => {
|
||||
const date = new Date('2024-07-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
@@ -285,19 +306,26 @@ describe('Content Schedule', () => {
|
||||
expect(matcher.match('backgroundkey022021')).to.be.true;
|
||||
});
|
||||
|
||||
it('allows background even yeared backgrounds in first half of year', () => {
|
||||
it('allows even yeared backgrounds in first half of year', () => {
|
||||
const date = new Date('2025-02-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
expect(matcher.match('backgroundkey022024')).to.be.true;
|
||||
expect(matcher.match('backgroundkey082022')).to.be.true;
|
||||
});
|
||||
|
||||
it('allows background odd yeared backgrounds in second half of year', () => {
|
||||
it('allows odd yeared backgrounds in second half of year', () => {
|
||||
const date = new Date('2024-08-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
expect(matcher.match('backgroundkey022023')).to.be.true;
|
||||
expect(matcher.match('backgroundkey082021')).to.be.true;
|
||||
});
|
||||
|
||||
it('allows odd yeared backgrounds in beginning of january', () => {
|
||||
const date = new Date('2025-01-06');
|
||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
expect(matcher.match('backgroundkey122024'), 'backgroundkey122024').to.be.true;
|
||||
expect(matcher.match('backgroundkey062023'), 'backgroundkey062022').to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeTravelers matcher', () => {
|
||||
|
||||
@@ -74,15 +74,10 @@ export async function getDocument (collectionName, doc) {
|
||||
}
|
||||
|
||||
before(done => {
|
||||
mongoose.connection.on('open', err => {
|
||||
if (err) return done(err);
|
||||
return resetHabiticaDB()
|
||||
.then(() => {
|
||||
mongoose.connection.once('open', async err => {
|
||||
if (err) throw err;
|
||||
await resetHabiticaDB();
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = {
|
||||
extends: [
|
||||
'habitrpg/lib/vue',
|
||||
],
|
||||
ignorePatterns: ['dist/', 'node_modules/'],
|
||||
ignorePatterns: ['dist/', 'node_modules/', '*.d.ts'],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
|
||||
286
website/client/package-lock.json
generated
286
website/client/package-lock.json
generated
@@ -3934,9 +3934,9 @@
|
||||
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
@@ -3946,7 +3946,7 @@
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
@@ -4140,6 +4140,33 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
|
||||
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
|
||||
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"get-intrinsic": "^1.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -4616,9 +4643,9 @@
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -5422,6 +5449,19 @@
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
|
||||
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz",
|
||||
"integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||
@@ -5464,9 +5504,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -5588,12 +5628,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -5611,6 +5648,17 @@
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
|
||||
"integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
|
||||
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
|
||||
@@ -6695,36 +6743,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
@@ -6733,6 +6781,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/array-flatten": {
|
||||
@@ -6877,12 +6929,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
@@ -7125,15 +7177,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
|
||||
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"dunder-proto": "^1.0.0",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.0.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -7253,11 +7310,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -7353,9 +7410,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -7383,9 +7440,9 @@
|
||||
"integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg=="
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
|
||||
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
@@ -8919,6 +8976,14 @@
|
||||
"markdown-it": "bin/markdown-it.js"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz",
|
||||
"integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
@@ -8958,9 +9023,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-source-map": {
|
||||
"version": "1.1.0",
|
||||
@@ -9862,9 +9930,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
|
||||
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -10310,9 +10381,9 @@
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
@@ -11129,11 +11200,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -11736,9 +11807,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -11771,6 +11842,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -11855,14 +11934,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -11951,13 +12030,68 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup, .Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi {
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
@@ -47,6 +48,10 @@
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Cryptid {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
||||
top: -16px !important;
|
||||
}
|
||||
|
||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi;
|
||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi, Cryptid;
|
||||
|
||||
@each $foolPet in $foolPets {
|
||||
.Pet.Pet-FlyingPig-#{$foolPet} {
|
||||
|
||||
@@ -101,8 +101,7 @@
|
||||
|
||||
.btn-secondary,
|
||||
.dropdown > .btn-secondary.dropdown-toggle:not(.btn-success),
|
||||
.show > .btn-secondary.dropdown-toggle:not(.btn-success)
|
||||
{
|
||||
.show > .btn-secondary.dropdown-toggle:not(.btn-success) {
|
||||
background: $white;
|
||||
border: 2px solid transparent;
|
||||
color: $gray-50;
|
||||
@@ -298,6 +297,16 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-flat,
|
||||
.dropdown > .btn-flat.dropdown-toggle:not(.btn-success),
|
||||
.show > .btn-flat.dropdown-toggle:not(.btn-success) {
|
||||
&.with-icon {
|
||||
.svg-icon.color {
|
||||
color: var(--icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,12 @@
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-min-width {
|
||||
.dropdown-menu {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// shared dropdown-item styles
|
||||
@@ -54,6 +59,8 @@
|
||||
color: $gray-50 !important;
|
||||
cursor: pointer;
|
||||
|
||||
--dropdown-item-hover-icon-color: #{$gray-200};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: inherit;
|
||||
@@ -88,7 +95,7 @@
|
||||
|
||||
&:not(:hover) {
|
||||
.with-icon .svg-icon {
|
||||
color: $gray-200;
|
||||
color: var(dropdown-item-hover-icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,7 +158,7 @@
|
||||
|
||||
// selectList.vue items sizing
|
||||
.selectListItem .dropdown-item {
|
||||
padding: 0.25rem 0.75rem;
|
||||
padding: 0.25rem 1rem 0.25rem 0.75rem;
|
||||
height: 32px;
|
||||
|
||||
&:active, &:hover, &:focus, &.active {
|
||||
|
||||
3
website/client/src/assets/svg/bluesky.svg
Normal file
3
website/client/src/assets/svg/bluesky.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4,0C1.79,0,0,1.79,0,4v16c0,2.21,1.79,4,4,4h16c2.21,0,4-1.79,4-4V4c0-2.21-1.79-4-4-4H4ZM12,11.57c-.72-1.49-2.7-4.26-4.53-5.63-1.32-.99-3.47-1.75-3.47.68,0,.49.28,4.08.44,4.66.57,2.03,2.65,2.55,4.5,2.23-3.24.55-4.06,2.36-2.28,4.17,3.38,3.44,4.85-.86,5.23-1.97h0s0,0,0,0c.07-.2.1-.29.1-.21,0-.08.03.01.1.22h0c.38,1.1,1.85,5.41,5.23,1.97,1.78-1.81.95-3.63-2.28-4.17,1.85.31,3.93-.2,4.5-2.23.16-.58.44-4.18.44-4.66,0-2.43-2.14-1.67-3.47-.68-1.83,1.37-3.81,4.14-4.53,5.63Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 572 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM18.36,8.74c0,.14,0,.29,0,.43A9.34,9.34,0,0,1,4,17a6.85,6.85,0,0,0,.79,0,6.57,6.57,0,0,0,4.07-1.4A3.29,3.29,0,0,1,5.8,13.39a4.1,4.1,0,0,0,.62,0,3.49,3.49,0,0,0,.86-.11,3.28,3.28,0,0,1-2.63-3.22v0a3.35,3.35,0,0,0,1.48.42A3.29,3.29,0,0,1,4.67,7.76,3.22,3.22,0,0,1,5.12,6.1a9.3,9.3,0,0,0,6.76,3.43,3.67,3.67,0,0,1-.08-.75,3.28,3.28,0,0,1,5.67-2.24,6.54,6.54,0,0,0,2.08-.79,3.22,3.22,0,0,1-1.44,1.8A6.67,6.67,0,0,0,20,7.05,7.31,7.31,0,0,1,18.36,8.74Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 622 B |
@@ -25,9 +25,9 @@
|
||||
<router-link to="/">
|
||||
Homepage
|
||||
</router-link>or
|
||||
<router-link :to="contactUsLink">
|
||||
<a href="mailto:admin@habitica.com">
|
||||
Contact Us
|
||||
</router-link>about the issue.
|
||||
</a>about the issue.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,12 +40,6 @@ import { mapState } from '@/libs/store';
|
||||
export default {
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn']),
|
||||
contactUsLink () {
|
||||
if (this.isUserLoggedIn) {
|
||||
return { name: 'guild', params: { groupId: 'a29da26b-37de-4a71-b0c6-48e72a900dac' } };
|
||||
}
|
||||
return { name: 'contact' };
|
||||
},
|
||||
retiredChatPage () {
|
||||
return this.$route.fullPath.indexOf('/groups') !== -1;
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-6 offset-3">
|
||||
<div class="shop_armoire"></div>
|
||||
<Sprite image-name="shop_armoire" />
|
||||
<p>{{ $t('armoireLastItem') }}</p>
|
||||
<p>{{ $t('armoireNotesEmpty') }}</p>
|
||||
</div>
|
||||
@@ -34,7 +34,12 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sprite,
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'armoire-empty');
|
||||
|
||||
@@ -95,7 +95,11 @@
|
||||
@click="clickDisableClasses(); close();"
|
||||
>{{ $t('optOutOfClasses') }}</span>
|
||||
</div>
|
||||
<span class="opt-out-description">{{ $t('optOutOfClassesText') }}</span>
|
||||
<div
|
||||
v-once
|
||||
class="opt-out-description"
|
||||
v-html="$t('optOutOfClassesText')"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div :class="questClass"></div>
|
||||
<Sprite :image-name="questClass" />
|
||||
</section>
|
||||
<!-- @TODO: Keep this? .checkboxinput(type='checkbox', v-model=
|
||||
'user.preferences.suppressModals.levelUp', @change='changeLevelupSuppress()')
|
||||
@@ -150,15 +150,12 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
|
||||
section.greyed {
|
||||
padding-bottom: 17px
|
||||
}
|
||||
|
||||
.scroll {
|
||||
margin: -11px auto 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Avatar from '../avatar';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
import { mapState } from '@/libs/store';
|
||||
import starGroup from '@/assets/svg/star-group.svg';
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
||||
@@ -173,6 +170,7 @@ const levelQuests = {
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
Sprite,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -191,7 +189,9 @@ export default {
|
||||
return this.user.stats.lvl in levelQuests;
|
||||
},
|
||||
questClass () {
|
||||
return `scroll inventory_quest_scroll_${levelQuests[this.user.stats.lvl]}`;
|
||||
const questKey = levelQuests[this.user.stats.lvl];
|
||||
if (questKey) return `inventory_quest_scroll_${questKey}`;
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<p v-html="$t('moreGearAchievements')"></p>
|
||||
<br>
|
||||
</div>
|
||||
<div class="shop_armoire"></div>
|
||||
<Sprite image-name="shop_armoire" />
|
||||
<p v-html="$t('armoireUnlocked')"></p>
|
||||
<br>
|
||||
<button
|
||||
@@ -87,11 +87,13 @@
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
import { mapState } from '@/libs/store';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
Sprite,
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
|
||||
@@ -92,8 +92,6 @@ export default {
|
||||
params: { userIdentifier },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
// the admin has requested that the same user be displayed again so reload the page
|
||||
// (e.g., if they changed their mind about changes they were making)
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
@@ -101,14 +99,16 @@ export default {
|
||||
|
||||
async loadUser (userIdentifier) {
|
||||
const id = userIdentifier || this.user._id;
|
||||
|
||||
this.$router.push({
|
||||
if (this.$router.currentRoute.name === 'adminPanelUser') {
|
||||
await this.$router.push({
|
||||
name: 'adminPanel',
|
||||
});
|
||||
}
|
||||
await this.$router.push({
|
||||
name: 'adminPanelUser',
|
||||
params: { userIdentifier: id },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
// the admin has requested that the same user be displayed again so reload the page
|
||||
// (e.g., if they changed their mind about changes they were making)
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
async saveHero ({ hero, msg = 'User', clearData }) {
|
||||
async saveHero ({
|
||||
hero,
|
||||
msg = 'User',
|
||||
clearData,
|
||||
reloadData,
|
||||
}) {
|
||||
await this.$store.dispatch('hall:updateHero', { heroDetails: hero });
|
||||
await this.$store.dispatch('snackbars:add', {
|
||||
title: '',
|
||||
@@ -14,6 +23,20 @@ export default {
|
||||
// The admin should re-fetch the data if they need to keep working on that user.
|
||||
this.$emit('clear-data');
|
||||
this.$router.push({ name: 'adminPanel' });
|
||||
} else if (reloadData) {
|
||||
if (this.$router.currentRoute.name === 'adminPanelUser') {
|
||||
await this.$router.push({
|
||||
name: 'adminPanel',
|
||||
});
|
||||
}
|
||||
await this.$router.push({
|
||||
name: 'adminPanelUser',
|
||||
params: { userIdentifier: hero._id },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
>
|
||||
Could not find any matching users.
|
||||
</div>
|
||||
<loading-spinner class="mx-auto mb-2" dark-color="true" v-if="isSearching" />
|
||||
<loading-spinner
|
||||
v-if="isSearching"
|
||||
class="mx-auto mb-2"
|
||||
dark-color="true"
|
||||
/>
|
||||
<div
|
||||
v-if="users.length > 0"
|
||||
class="list-group"
|
||||
@@ -59,6 +63,10 @@ export default {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.userIdentifier = to.params.userIdentifier;
|
||||
next();
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
userIdentifier: '',
|
||||
@@ -70,10 +78,6 @@ export default {
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.userIdentifier = to.params.userIdentifier;
|
||||
next();
|
||||
},
|
||||
watch: {
|
||||
userIdentifier () {
|
||||
this.isSearching = true;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<li
|
||||
v-for="item in achievements"
|
||||
:key="item.path"
|
||||
v-b-tooltip.hover="item.notes"
|
||||
>
|
||||
<form @submit.prevent="saveItem(item)">
|
||||
<span
|
||||
@@ -27,7 +28,7 @@
|
||||
{{ item.value }}
|
||||
</span>
|
||||
:
|
||||
{{ item.text || item.key }}
|
||||
{{ item.text || item.key }} - <i> {{ item.key }} </i>
|
||||
</span>
|
||||
|
||||
<div
|
||||
@@ -68,6 +69,7 @@
|
||||
<li
|
||||
v-for="item in nestedAchievements[achievementType]"
|
||||
:key="item.path"
|
||||
v-b-tooltip.hover="item.notes"
|
||||
>
|
||||
<form @submit.prevent="saveItem(item)">
|
||||
<span
|
||||
@@ -78,7 +80,7 @@
|
||||
{{ item.value }}
|
||||
</span>
|
||||
:
|
||||
{{ item.text || item.key }}
|
||||
{{ item.text || item.key }} - <i> {{ item.key }} </i>
|
||||
</span>
|
||||
|
||||
<div
|
||||
@@ -143,79 +145,28 @@ function getText (achievementItem) {
|
||||
}
|
||||
const { titleKey } = achievementItem;
|
||||
if (titleKey !== undefined) {
|
||||
return i18n.t(titleKey, 'en');
|
||||
return i18n.t(titleKey);
|
||||
}
|
||||
const { singularTitleKey } = achievementItem;
|
||||
if (singularTitleKey !== undefined) {
|
||||
return i18n.t(singularTitleKey, 'en');
|
||||
return i18n.t(singularTitleKey);
|
||||
}
|
||||
return achievementItem.key;
|
||||
}
|
||||
|
||||
function collateItemData (self) {
|
||||
const achievements = [];
|
||||
const nestedAchievements = {};
|
||||
const basePath = 'achievements';
|
||||
const ownedAchievements = self.hero.achievements;
|
||||
const allAchievements = content.achievements;
|
||||
|
||||
for (const key of Object.keys(ownedAchievements)) {
|
||||
const value = ownedAchievements[key];
|
||||
if (typeof value === 'object') {
|
||||
nestedAchievements[key] = [];
|
||||
for (const nestedKey of Object.keys(value)) {
|
||||
const valueIsInteger = self.integerTypes.includes(key);
|
||||
let text = nestedKey;
|
||||
if (allAchievements[key] && allAchievements[key][nestedKey]) {
|
||||
text = getText(allAchievements[key][nestedKey]);
|
||||
function getNotes (achievementItem, count) {
|
||||
if (achievementItem === undefined) {
|
||||
return '';
|
||||
}
|
||||
nestedAchievements[key].push({
|
||||
key: nestedKey,
|
||||
text,
|
||||
achievementType: key,
|
||||
modified: false,
|
||||
path: `${basePath}.${key}.${nestedKey}`,
|
||||
value: value[nestedKey],
|
||||
valueIsInteger,
|
||||
});
|
||||
const { textKey } = achievementItem;
|
||||
if (textKey !== undefined) {
|
||||
return i18n.t(textKey, { count });
|
||||
}
|
||||
} else {
|
||||
const valueIsInteger = self.integerTypes.includes(key);
|
||||
achievements.push({
|
||||
key,
|
||||
text: getText(allAchievements[key]),
|
||||
modified: false,
|
||||
path: `${basePath}.${key}`,
|
||||
value: ownedAchievements[key],
|
||||
valueIsInteger,
|
||||
});
|
||||
const { singularTextKey } = achievementItem;
|
||||
if (singularTextKey !== undefined) {
|
||||
return i18n.t(singularTextKey, { count });
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(allAchievements)) {
|
||||
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
|
||||
if (ownedAchievements[key] === undefined) {
|
||||
const valueIsInteger = self.integerTypes.includes(key);
|
||||
achievements.push({
|
||||
key,
|
||||
text: getText(allAchievements[key]),
|
||||
modified: false,
|
||||
path: `${basePath}.${key}`,
|
||||
value: valueIsInteger ? 0 : false,
|
||||
valueIsInteger,
|
||||
neverOwned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.achievements = achievements;
|
||||
self.nestedAchievements = nestedAchievements;
|
||||
}
|
||||
|
||||
function resetData (self) {
|
||||
collateItemData(self);
|
||||
self.nestedAchievementKeys.forEach(itemType => { self.expandItemType[itemType] = false; });
|
||||
return '';
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -241,26 +192,34 @@ export default {
|
||||
},
|
||||
nestedAchievementKeys: ['quests', 'ultimateGearSets'],
|
||||
integerTypes: ['streak', 'perfect', 'birthday', 'habiticaDays', 'habitSurveys', 'habitBirthdays',
|
||||
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests'],
|
||||
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests',
|
||||
'rebirths', 'rebirthLevel', 'greeting', 'spookySparkles', 'nye', 'costumeContests', 'congrats',
|
||||
'getwell', 'beastMasterCount', 'mountMasterCount', 'triadBingoCount',
|
||||
],
|
||||
cardTypes: ['greeting', 'birthday', 'valentine', 'goodluck', 'thankyou', 'greeting', 'nye',
|
||||
'congrats', 'getwell'],
|
||||
achievements: [],
|
||||
nestedAchievements: {},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
this.resetData();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
this.resetData();
|
||||
},
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
// prepare the item's new value and path for being saved
|
||||
this.hero.achievementPath = item.path;
|
||||
this.hero.achievementVal = item.value;
|
||||
|
||||
await this.saveHero({ hero: this.hero, msg: item.path });
|
||||
await this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
achievementPath: item.path,
|
||||
achievementVal: item.value,
|
||||
},
|
||||
msg: item.path,
|
||||
});
|
||||
item.modified = false;
|
||||
},
|
||||
enableValueChange (item) {
|
||||
@@ -270,6 +229,85 @@ export default {
|
||||
item.value = !item.value;
|
||||
}
|
||||
},
|
||||
resetData () {
|
||||
this.collateItemData();
|
||||
this.nestedAchievementKeys.forEach(itemType => { this.expandItemType[itemType] = false; });
|
||||
},
|
||||
collateItemData () {
|
||||
const achievements = [];
|
||||
const nestedAchievements = {};
|
||||
const basePath = 'achievements';
|
||||
const ownedAchievements = this.hero.achievements;
|
||||
const allAchievements = content.achievements;
|
||||
|
||||
const ownedKeys = Object.keys(ownedAchievements).sort();
|
||||
for (const key of ownedKeys) {
|
||||
const value = ownedAchievements[key];
|
||||
let contentKey = key;
|
||||
if (this.cardTypes.indexOf(key) !== -1) {
|
||||
contentKey += 'Cards';
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
nestedAchievements[key] = [];
|
||||
for (const nestedKey of Object.keys(value)) {
|
||||
const valueIsInteger = this.integerTypes.includes(key);
|
||||
let text = nestedKey;
|
||||
if (allAchievements[key] && allAchievements[key][contentKey]) {
|
||||
text = getText(allAchievements[key][contentKey]);
|
||||
}
|
||||
let notes = '';
|
||||
if (allAchievements[key] && allAchievements[key][contentKey]) {
|
||||
notes = getNotes(allAchievements[key][contentKey], ownedAchievements[key]);
|
||||
}
|
||||
nestedAchievements[key].push({
|
||||
key: nestedKey,
|
||||
text,
|
||||
notes,
|
||||
achievementType: key,
|
||||
modified: false,
|
||||
path: `${basePath}.${key}.${nestedKey}`,
|
||||
value: value[nestedKey],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const valueIsInteger = this.integerTypes.includes(key);
|
||||
achievements.push({
|
||||
key,
|
||||
text: getText(allAchievements[contentKey]),
|
||||
notes: getNotes(allAchievements[contentKey], ownedAchievements[key]),
|
||||
modified: false,
|
||||
path: `${basePath}.${key}`,
|
||||
value: ownedAchievements[key],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allKeys = Object.keys(allAchievements).sort();
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
|
||||
const ownedKey = key.replace('Cards', '');
|
||||
if (ownedAchievements[ownedKey] === undefined) {
|
||||
const valueIsInteger = this.integerTypes.includes(ownedKey);
|
||||
achievements.push({
|
||||
key: ownedKey,
|
||||
text: getText(allAchievements[key]),
|
||||
notes: getNotes(allAchievements[key], 0),
|
||||
modified: false,
|
||||
path: `${basePath}.${ownedKey}`,
|
||||
value: valueIsInteger ? 0 : false,
|
||||
valueIsInteger,
|
||||
neverOwned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.achievements = achievements;
|
||||
this.nestedAchievements = nestedAchievements;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Contributor details', clearData: true })">
|
||||
<form
|
||||
@submit.prevent="saveHero({ hero: {
|
||||
_id: hero._id,
|
||||
contributor: hero.contributor,
|
||||
secret: hero.secret,
|
||||
permissions: hero.permissions,
|
||||
}, msg: 'Contributor details', clearData: true })"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -8,6 +15,12 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Contributor Details
|
||||
<b
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -104,13 +117,16 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -190,6 +206,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Authentication' })">
|
||||
<form
|
||||
@submit.prevent="saveHero({ hero: {
|
||||
_id: hero._id,
|
||||
auth: hero.auth,
|
||||
preferences: hero.preferences,
|
||||
}, msg: 'Authentication' })"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -38,7 +44,10 @@
|
||||
<strong v-else>No</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="cronError" class="form-group row">
|
||||
<div
|
||||
v-if="cronError"
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">lastCron value:</label>
|
||||
<strong>{{ hero.lastCron | formatDate }}</strong>
|
||||
<br>
|
||||
@@ -53,12 +62,12 @@
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<strong>
|
||||
{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||
<button
|
||||
<a
|
||||
class="btn btn-warning btn-sm ml-4"
|
||||
@click="resetCron()"
|
||||
>
|
||||
Reset Cron to Yesterday
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -110,13 +119,14 @@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">API Token</label>
|
||||
<div class="col-sm-9">
|
||||
<button
|
||||
<a
|
||||
href="#"
|
||||
value="Change API Token"
|
||||
class="btn btn-danger"
|
||||
@click="changeApiToken()"
|
||||
>
|
||||
Change API Token
|
||||
</button>
|
||||
</a>
|
||||
<div
|
||||
v-if="tokenModified"
|
||||
>
|
||||
@@ -268,13 +278,24 @@ export default {
|
||||
return false;
|
||||
},
|
||||
async changeApiToken () {
|
||||
this.hero.changeApiToken = true;
|
||||
await this.saveHero({ hero: this.hero, msg: 'API Token' });
|
||||
await this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
changeApiToken: true,
|
||||
},
|
||||
msg: 'API Token',
|
||||
});
|
||||
this.tokenModified = true;
|
||||
},
|
||||
resetCron () {
|
||||
this.hero.resetCron = true;
|
||||
this.saveHero({ hero: this.hero, msg: 'Last Cron', clearData: true });
|
||||
this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
resetCron: true,
|
||||
},
|
||||
msg: 'Last Cron',
|
||||
clearData: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
:
|
||||
<span :class="{ ownedItem: !item.neverOwned }">{{ item.text }}</span>
|
||||
</span>
|
||||
{{ item.set }}
|
||||
- {{ itemType }}.{{item.key}} - <i> {{ item.set }}</i>
|
||||
|
||||
<div
|
||||
v-if="item.modified"
|
||||
@@ -232,11 +232,14 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
// prepare the item's new value and path for being saved
|
||||
this.hero.purchasedPath = item.path;
|
||||
this.hero.purchasedVal = item.value;
|
||||
|
||||
await this.saveHero({ hero: this.hero, msg: item.path });
|
||||
await this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
purchasedPath: item.path,
|
||||
purchasedVal: item.value,
|
||||
},
|
||||
msg: item.path,
|
||||
});
|
||||
item.modified = false;
|
||||
},
|
||||
enableValueChange (item) {
|
||||
|
||||
@@ -15,10 +15,17 @@
|
||||
<privileges-and-gems
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.flags, unModifiedHero.flags],
|
||||
[hero.auth, unModifiedHero.auth],
|
||||
[hero.balance, unModifiedHero.balance],
|
||||
[hero.secret, unModifiedHero.secret])"
|
||||
/>
|
||||
|
||||
<subscription-and-perks
|
||||
:hero="hero"
|
||||
:group-plans="groupPlans"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.purchased.plan,
|
||||
unModifiedHero.purchased.plan])"
|
||||
/>
|
||||
|
||||
<cron-and-auth
|
||||
@@ -29,6 +36,7 @@
|
||||
<user-profile
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.profile, unModifiedHero.profile])"
|
||||
/>
|
||||
|
||||
<party-and-quest
|
||||
@@ -47,6 +55,12 @@
|
||||
:preferences="hero.preferences"
|
||||
/>
|
||||
|
||||
<stats
|
||||
:hero="hero"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.stats, unModifiedHero.stats])"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<items-owned
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
@@ -67,8 +81,18 @@
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<user-history
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<contributor-details
|
||||
:hero="hero"
|
||||
:hasUnsavedChanges="hasUnsavedChanges(
|
||||
[hero.contributor, unModifiedHero.contributor],
|
||||
[hero.permissions, unModifiedHero.permissions],
|
||||
[hero.secret, unModifiedHero.secret],
|
||||
)"
|
||||
:reset-counter="resetCounter"
|
||||
@clear-data="clearData"
|
||||
/>
|
||||
@@ -109,6 +133,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import isEqualWith from 'lodash/isEqualWith';
|
||||
import BasicDetails from './basicDetails';
|
||||
import ItemsOwned from './itemsOwned';
|
||||
import CronAndAuth from './cronAndAuth';
|
||||
@@ -121,6 +146,8 @@ import Transactions from './transactions';
|
||||
import SubscriptionAndPerks from './subscriptionAndPerks';
|
||||
import CustomizationsOwned from './customizationsOwned.vue';
|
||||
import Achievements from './achievements.vue';
|
||||
import UserHistory from './userHistory.vue';
|
||||
import Stats from './stats.vue';
|
||||
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
@@ -135,6 +162,8 @@ export default {
|
||||
PrivilegesAndGems,
|
||||
ContributorDetails,
|
||||
Transactions,
|
||||
UserHistory,
|
||||
Stats,
|
||||
SubscriptionAndPerks,
|
||||
UserProfile,
|
||||
Achievements,
|
||||
@@ -148,8 +177,10 @@ export default {
|
||||
return {
|
||||
userIdentifier: '',
|
||||
resetCounter: 0,
|
||||
unModifiedHero: {},
|
||||
hero: {},
|
||||
party: {},
|
||||
groupPlans: [],
|
||||
hasParty: false,
|
||||
partyNotExistError: false,
|
||||
adminHasPrivForParty: true,
|
||||
@@ -168,6 +199,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
clearData () {
|
||||
this.unModifiedHero = {};
|
||||
this.hero = {};
|
||||
},
|
||||
|
||||
@@ -176,6 +208,7 @@ export default {
|
||||
this.$emit('changeUserIdentifier', id); // change user identifier in Admin Panel's form
|
||||
|
||||
this.hero = await this.$store.dispatch('hall:getHero', { uuid: id });
|
||||
this.unModifiedHero = JSON.parse(JSON.stringify(this.hero));
|
||||
|
||||
if (!this.hero.flags) {
|
||||
this.hero.flags = {
|
||||
@@ -206,8 +239,38 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hero.purchased.plan.planId === 'group_plan_auto') {
|
||||
try {
|
||||
this.groupPlans = await this.$store.dispatch('hall:getHeroGroupPlans', { heroId: this.hero._id });
|
||||
} catch (e) {
|
||||
this.groupPlans = [];
|
||||
}
|
||||
}
|
||||
|
||||
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
||||
},
|
||||
hasUnsavedChanges (...comparisons) {
|
||||
for (const index in comparisons) {
|
||||
if (index && comparisons[index]) {
|
||||
const objs = comparisons[index];
|
||||
const obj1 = objs[0];
|
||||
const obj2 = objs[1];
|
||||
if (!isEqualWith(obj1, obj2, (x, y) => {
|
||||
if (typeof x === 'object' && typeof y === 'object') {
|
||||
return undefined;
|
||||
}
|
||||
if (x === false && y === undefined) {
|
||||
// Special case for checkboxes
|
||||
return true;
|
||||
}
|
||||
return x == y; // eslint-disable-line eqeqeq
|
||||
})) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -269,16 +269,19 @@ export default {
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
// prepare the item's new value and path for being saved
|
||||
this.hero.itemPath = item.path;
|
||||
const toSave = {
|
||||
_id: this.hero._id,
|
||||
};
|
||||
toSave.itemPath = item.path;
|
||||
if (item.value === null) {
|
||||
this.hero.itemVal = 'null';
|
||||
toSave.itemVal = 'null';
|
||||
} else if (item.value === false) {
|
||||
this.hero.itemVal = 'false';
|
||||
toSave.itemVal = 'false';
|
||||
} else {
|
||||
this.hero.itemVal = item.value;
|
||||
toSave.itemVal = item.value;
|
||||
}
|
||||
|
||||
await this.saveHero({ hero: this.hero, msg: item.key });
|
||||
await this.saveHero({ hero: toSave, msg: item.key });
|
||||
item.neverOwned = false;
|
||||
item.modified = false;
|
||||
},
|
||||
|
||||
@@ -31,22 +31,41 @@
|
||||
v-html="questErrors"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Party:
|
||||
<span v-if="userHasParty">
|
||||
yes: party ID {{ groupPartyData._id }},
|
||||
member count {{ groupPartyData.memberCount }} (may be wrong)
|
||||
<br>
|
||||
<div v-if="userHasParty">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Party ID
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData._id }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Estimated Member Count
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData.memberCount }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Leader
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
<span v-if="userIsPartyLeader">User is the party leader</span>
|
||||
<span v-else>Party leader is
|
||||
<router-link :to="{'name': 'userProfile', 'params': {'userId': groupPartyData.leader}}">
|
||||
{{ groupPartyData.leader }}
|
||||
</router-link>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>no</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-danger"
|
||||
@click="removeFromParty()">Remove from Party</div>
|
||||
</div>
|
||||
<strong v-else>User is not in a party.</strong>
|
||||
<div class="subsection-start">
|
||||
<p v-html="questStatus"></p>
|
||||
</div>
|
||||
@@ -56,6 +75,7 @@
|
||||
|
||||
<script>
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
function determineQuestStatus (self) {
|
||||
// Quest data is in the user doc and party doc. They can be out of sync.
|
||||
@@ -271,6 +291,7 @@ function resetData (self) {
|
||||
}
|
||||
|
||||
export default {
|
||||
mixins: [saveHero],
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
@@ -318,5 +339,14 @@ export default {
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
removeFromParty () {
|
||||
this.saveHero({
|
||||
hero: { _id: this.userId, removeFromParty: true },
|
||||
msg: 'Removed from party',
|
||||
reloadData: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<form @submit.prevent="saveHero({hero: {
|
||||
_id: hero._id,
|
||||
flags: hero.flags,
|
||||
balance: hero.balance,
|
||||
auth: hero.auth,
|
||||
secret: hero.secret,
|
||||
}, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -8,6 +14,9 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Privileges, Gem Balance
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -117,13 +126,16 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -169,6 +181,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="form-group row">
|
||||
<label
|
||||
class="col-sm-3 col-form-label"
|
||||
:class="color"
|
||||
>{{ label }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
:value="value"
|
||||
class="form-control"
|
||||
type="number"
|
||||
:step="step"
|
||||
:max="max"
|
||||
:min="min"
|
||||
@input="$emit('input', parseInt($event.target.value, 10))"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.about-row {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.red-label {
|
||||
color: $red_100;
|
||||
}
|
||||
.blue-label {
|
||||
color: $blue_100;
|
||||
}
|
||||
.purple-label {
|
||||
color: $purple_300;
|
||||
}
|
||||
.yellow-label {
|
||||
color: $yellow_50;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'input',
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'text-label',
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
step: {
|
||||
type: String,
|
||||
default: 'any',
|
||||
},
|
||||
min: {
|
||||
},
|
||||
max: {
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
286
website/client/src/components/admin-panel/user-support/stats.vue
Normal file
286
website/client/src/components/admin-panel/user-support/stats.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitClicked()">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Stats
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<stats-row
|
||||
label="Health"
|
||||
color="red-label"
|
||||
:max="maxHealth"
|
||||
v-model="hero.stats.hp" />
|
||||
<stats-row
|
||||
label="Experience"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.exp" />
|
||||
<stats-row
|
||||
label="Mana"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.mp" />
|
||||
<stats-row
|
||||
label="Level"
|
||||
step="1"
|
||||
min="0"
|
||||
:max="maxLevelHardCap"
|
||||
v-model="hero.stats.lvl" />
|
||||
<stats-row
|
||||
label="Gold"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.gp" />
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Selected Class</label>
|
||||
<div class="col-sm-9">
|
||||
<select
|
||||
id="selectedClass"
|
||||
v-model="hero.stats.class"
|
||||
class="form-control"
|
||||
:disabled="hero.stats.lvl < 10"
|
||||
>
|
||||
<option value="warrior">Warrior</option>
|
||||
<option value="wizard">Mage</option>
|
||||
<option value="healer">Healer</option>
|
||||
<option value="rogue">Rogue</option>
|
||||
</select>
|
||||
<small>
|
||||
When changing class, players usually need stat points deallocated as well.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Stat Points</h3>
|
||||
<stats-row
|
||||
label="Unallocated"
|
||||
min="0"
|
||||
step="1"
|
||||
:max="maxStatPoints"
|
||||
v-model="hero.stats.points" />
|
||||
<stats-row
|
||||
label="Strength"
|
||||
color="red-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.str" />
|
||||
<stats-row
|
||||
label="Intelligence"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.int" />
|
||||
<stats-row
|
||||
label="Perception"
|
||||
color="purple-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.per" />
|
||||
<stats-row
|
||||
label="Constitution"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.con" />
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
@click="deallocateStatPoints">
|
||||
Deallocate all stat points
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" v-if="statPointsIncorrect">
|
||||
<div class="offset-sm-3 col-sm-9 text-danger">
|
||||
Error: Sum of stat points should equal the users level
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Buffs</h3>
|
||||
<stats-row
|
||||
label="Strength"
|
||||
color="red-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.str" />
|
||||
<stats-row
|
||||
label="Intelligence"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.int" />
|
||||
<stats-row
|
||||
label="Perception"
|
||||
color="purple-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.per" />
|
||||
<stats-row
|
||||
label="Constitution"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.con" />
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
@click="resetBuffs">
|
||||
Reset Buffs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.about-row {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {
|
||||
MAX_HEALTH,
|
||||
MAX_STAT_POINTS,
|
||||
MAX_LEVEL_HARD_CAP,
|
||||
MAX_FIELD_HARD_CAP,
|
||||
} from '@/../../common/script/constants';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
import StatsRow from './stats-row';
|
||||
|
||||
function resetData (self) {
|
||||
self.expand = false;
|
||||
}
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
StatsRow,
|
||||
},
|
||||
mixins: [
|
||||
userStateMixin,
|
||||
saveHero,
|
||||
],
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
statPointsIncorrect () {
|
||||
if (this.hero.stats.lvl >= 10) {
|
||||
return (parseInt(this.hero.stats.points, 10)
|
||||
+ parseInt(this.hero.stats.str, 10)
|
||||
+ parseInt(this.hero.stats.int, 10)
|
||||
+ parseInt(this.hero.stats.per, 10)
|
||||
+ parseInt(this.hero.stats.con, 10)
|
||||
) !== this.hero.stats.lvl;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
maxHealth: MAX_HEALTH,
|
||||
maxStatPoints: MAX_STAT_POINTS,
|
||||
maxLevelHardCap: MAX_LEVEL_HARD_CAP,
|
||||
maxFieldHardCap: MAX_FIELD_HARD_CAP,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
submitClicked () {
|
||||
if (this.statPointsIncorrect) {
|
||||
return;
|
||||
}
|
||||
this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
stats: this.hero.stats,
|
||||
},
|
||||
msg: 'Stats',
|
||||
});
|
||||
},
|
||||
resetBuffs () {
|
||||
this.hero.stats.buffs = {
|
||||
str: 0,
|
||||
int: 0,
|
||||
per: 0,
|
||||
con: 0,
|
||||
};
|
||||
},
|
||||
deallocateStatPoints () {
|
||||
this.hero.stats.points = this.hero.stats.lvl;
|
||||
this.hero.stats.str = 0;
|
||||
this.hero.stats.int = 0;
|
||||
this.hero.stats.per = 0;
|
||||
this.hero.stats.con = 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,30 +1,135 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
|
||||
<form
|
||||
@submit.prevent="saveHero({ hero: {
|
||||
_id: hero._id,
|
||||
purchased: hero.purchased
|
||||
}, msg: 'Subscription Perks' })"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<div class="card-header"
|
||||
@click="expand = !expand">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{ 'open': expand }"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Subscription, Monthly Perks
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div v-if="hero.purchased.plan.paymentMethod">
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Payment method:
|
||||
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input v-model="hero.purchased.plan.paymentMethod"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-if="!isRegularPaymentMethod"
|
||||
>
|
||||
<select
|
||||
v-else
|
||||
v-model="hero.purchased.plan.paymentMethod"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="groupPlan">Group Plan</option>
|
||||
<option value="Stripe">Stripe</option>
|
||||
<option value="Apple">Apple</option>
|
||||
<option value="Google">Google</option>
|
||||
<option value="Amazon Payments">Amazon</option>
|
||||
<option value="PayPal">PayPal</option>
|
||||
<option value="Gift">Gift</option>
|
||||
<option value="">Clear out</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Payment schedule:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input v-model="hero.purchased.plan.planId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-if="!isRegularPlanId"
|
||||
>
|
||||
<select
|
||||
v-else
|
||||
v-model="hero.purchased.plan.planId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="basic_earned">Monthly recurring</option>
|
||||
<option value="basic_3mo">3 Months recurring</option>
|
||||
<option value="basic_6mo">6 Months recurring</option>
|
||||
<option value="basic_12mo">12 Months recurring</option>
|
||||
<option value="group_monthly">Group Plan (legacy)</option>
|
||||
<option value="group_plan_auto">Group Plan (auto)</option>
|
||||
<option value="">Clear out</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Customer ID:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.customerId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="hero.purchased.plan.planId === 'group_plan_auto'">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Group Plan Memberships:
|
||||
</label>
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<loading-spinner
|
||||
v-if="!groupPlans"
|
||||
dark-color=true
|
||||
/>
|
||||
<b
|
||||
v-else-if="groupPlans.length === 0"
|
||||
class="text-danger col-form-label"
|
||||
>User is not part of an active group plan!</b>
|
||||
<div
|
||||
v-else
|
||||
v-for="group in groupPlans"
|
||||
:key="group._id"
|
||||
class="card mb-2">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ group.name }}
|
||||
<small class="float-right">{{ group._id }}</small>
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
<strong>Leader: </strong>
|
||||
<a
|
||||
v-if="group.leader !== hero._id"
|
||||
@click="switchUser(group.leader)"
|
||||
>{{ group.leader }}</a>
|
||||
<strong v-else class="text-success">This user</strong>
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<strong>Members: </strong> {{ group.memberCount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.planId">
|
||||
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"
|
||||
@@ -85,8 +190,18 @@
|
||||
<strong class="input-group-text">
|
||||
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||
</strong>
|
||||
<a class="btn btn-danger"
|
||||
href="#"
|
||||
v-b-modal.sub_termination_modal
|
||||
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId">
|
||||
Terminate
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<small v-if="!hero.purchased.plan.dateTerminated
|
||||
&& hero.purchased.plan.planId" class="text-success">
|
||||
The subscription does not have a termination date and is active.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -101,6 +216,35 @@
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
<small class="text-secondary">
|
||||
Cumulative subscribed months across subscription periods.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Extra months:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="hero.purchased.plan.extraMonths"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<a class="btn btn-warning"
|
||||
@click="applyExtraMonths"
|
||||
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0">
|
||||
Apply Credit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-secondary">
|
||||
Additional credit that is applied if a subscription is cancelled.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -174,10 +318,6 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.extraMonths > 0">
|
||||
Additional credit (applied upon cancellation):
|
||||
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Mystery Items:
|
||||
@@ -199,18 +339,64 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="beginGroupPlanConvert">
|
||||
Begin converting to group plan subscription
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="isConvertingToGroupPlan">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Group Plan group ID:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="groupPlanID"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
@click="saveClicked"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal id="sub_termination_modal" title="Set Termination Date">
|
||||
<p>
|
||||
You can set the sub benefit termination date to today or to the last
|
||||
day of the current billing cycle. Any extra subscription credit will
|
||||
then be processed and automatically added onto the selected date.
|
||||
</p>
|
||||
<template #modal-footer>
|
||||
<div class="mt-3 btn btn-secondary" @click="$bvModal.hide('sub_termination_modal')">
|
||||
Close
|
||||
</div>
|
||||
<div class="mt-3 btn btn-danger" @click="terminateSubscription()">
|
||||
Set to Today
|
||||
</div>
|
||||
<div class="mt-3 btn btn-danger" @click="terminateSubscription(todayWithRemainingCycle)">
|
||||
Set to {{ todayWithRemainingCycle.utc().format('MM/DD/YYYY') }}
|
||||
</div>
|
||||
</template>
|
||||
</b-modal>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -231,21 +417,38 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import isUUID from 'validator/es/lib/isUUID';
|
||||
import moment from 'moment';
|
||||
import { getPlanContext } from '@/../../common/script/cron';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import subscriptionBlocks from '../../../../../common/script/content/subscriptionBlocks';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
export default {
|
||||
mixins: [saveHero],
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
},
|
||||
props: {
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
groupPlans: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
isConvertingToGroupPlan: false,
|
||||
groupPlanID: '',
|
||||
subscriptionBlocks,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -255,6 +458,30 @@ export default {
|
||||
if (!currentPlanContext.nextHourglassDate) return 'N/A';
|
||||
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
|
||||
},
|
||||
isRegularPlanId () {
|
||||
return this.subscriptionBlocks[this.hero.purchased.plan.planId] !== undefined;
|
||||
},
|
||||
isRegularPaymentMethod () {
|
||||
return [
|
||||
'groupPlan',
|
||||
'Group Plan',
|
||||
'Stripe',
|
||||
'Apple',
|
||||
'Google',
|
||||
'Amazon Payments',
|
||||
'PayPal',
|
||||
'Gift',
|
||||
].includes(this.hero.purchased.plan.paymentMethod);
|
||||
},
|
||||
todayWithRemainingCycle () {
|
||||
const now = moment();
|
||||
const monthCount = subscriptionBlocks[this.hero.purchased.plan.planId].months;
|
||||
const terminationDate = moment(this.hero.purchased.plan.dateCurrentTypeCreated || new Date());
|
||||
while (terminationDate.isBefore(now)) {
|
||||
terminationDate.add(monthCount, 'months');
|
||||
}
|
||||
return terminationDate;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dateFormat (date) {
|
||||
@@ -263,6 +490,46 @@ export default {
|
||||
}
|
||||
return moment(date).format('YYYY/MM/DD');
|
||||
},
|
||||
terminateSubscription (terminationDate) {
|
||||
if (terminationDate) {
|
||||
this.hero.purchased.plan.dateTerminated = terminationDate.utc().format();
|
||||
} else {
|
||||
this.hero.purchased.plan.dateTerminated = moment(new Date()).utc().format();
|
||||
}
|
||||
this.applyExtraMonths();
|
||||
this.saveHero({ hero: this.hero, msg: 'Subscription Termination', reloadData: true });
|
||||
},
|
||||
applyExtraMonths () {
|
||||
if (this.hero.purchased.plan.extraMonths > 0 || this.hero.purchased.plan.extraMonths !== '0') {
|
||||
const date = moment(this.hero.purchased.plan.dateTerminated || new Date());
|
||||
const extraMonths = Math.max(this.hero.purchased.plan.extraMonths, 0);
|
||||
const extraDays = Math.ceil(30.5 * extraMonths);
|
||||
this.hero.purchased.plan.dateTerminated = date.add(extraDays, 'days').utc().format();
|
||||
this.hero.purchased.plan.extraMonths = 0;
|
||||
}
|
||||
},
|
||||
beginGroupPlanConvert () {
|
||||
this.isConvertingToGroupPlan = true;
|
||||
this.hero.purchased.plan.owner = '';
|
||||
},
|
||||
saveClicked (e) {
|
||||
e.preventDefault();
|
||||
if (this.isConvertingToGroupPlan) {
|
||||
if (!isUUID(this.groupPlanID)) {
|
||||
alert('Invalid group ID');
|
||||
return;
|
||||
}
|
||||
this.hero.purchased.plan.convertToGroupPlan = this.groupPlanID;
|
||||
this.saveHero({ hero: this.hero, msg: 'Group Plan Subscription', reloadData: true });
|
||||
} else {
|
||||
this.saveHero({ hero: this.hero, msg: 'Subscription Perks', reloadData: true });
|
||||
}
|
||||
},
|
||||
switchUser (id) {
|
||||
if (window.confirm('Switch to this user?')) {
|
||||
this.$emit('changeUserIdentifier', id);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="toggleHistoryOpen"
|
||||
>
|
||||
User History
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div>
|
||||
<div class="clearfix">
|
||||
<div class="mb-4 float-left">
|
||||
<button
|
||||
class="page-header btn-flat tab-button textCondensed"
|
||||
:class="{'active': selectedTab === 'armoire'}"
|
||||
@click="selectTab('armoire')"
|
||||
>
|
||||
Armoire
|
||||
</button>
|
||||
<button
|
||||
class="page-header btn-flat tab-button textCondensed"
|
||||
:class="{'active': selectedTab === 'questInvites'}"
|
||||
@click="selectTab('questInvites')"
|
||||
>
|
||||
Quest Invitations
|
||||
</button>
|
||||
<button
|
||||
class="page-header btn-flat tab-button textCondensed"
|
||||
:class="{'active': selectedTab === 'cron'}"
|
||||
@click="selectTab('cron')"
|
||||
>
|
||||
Cron
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
v-if="selectedTab === 'armoire'"
|
||||
class="col-12"
|
||||
>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
{{ $t('timestamp') }}
|
||||
</th>
|
||||
<th v-once>
|
||||
Client
|
||||
</th>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
Received
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in armoire"
|
||||
:key="entry.timestamp"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-b-tooltip.hover="entry.timestamp"
|
||||
>{{ entry.timestamp | timeAgo }}</span>
|
||||
</td>
|
||||
<td>{{ entry.client }}</td>
|
||||
<td>{{ entry.reward }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedTab === 'questInvites'"
|
||||
class="col-12"
|
||||
>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
{{ $t('timestamp') }}
|
||||
</th>
|
||||
<th v-once>
|
||||
Client
|
||||
</th>
|
||||
<th v-once>
|
||||
Quest Key
|
||||
</th>
|
||||
<th v-once>
|
||||
Response
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in questInviteResponses"
|
||||
:key="entry.timestamp"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-b-tooltip.hover="entry.timestamp"
|
||||
>{{ entry.timestamp | timeAgo }}</span>
|
||||
</td>
|
||||
<td>{{ entry.client }}</td>
|
||||
<td>{{ entry.quest }}</td>
|
||||
<td>{{ questInviteResponseText(entry.response) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedTab === 'cron'"
|
||||
class="col-12"
|
||||
>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
{{ $t('timestamp') }}
|
||||
</th>
|
||||
<th v-once>
|
||||
Client
|
||||
</th>
|
||||
<th v-once>
|
||||
Checkin Count
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in cron"
|
||||
:key="entry.timestamp"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-b-tooltip.hover="entry.timestamp"
|
||||
>{{ entry.timestamp | timeAgo }}</span>
|
||||
</td>
|
||||
<td>{{ entry.client }}</td>
|
||||
<td>{{ entry.checkinCount }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.page-header.btn-flat {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
height: 2rem;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-stretch: condensed;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
color: $gray-10;
|
||||
|
||||
margin-right: 1.125rem;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 2.5rem;
|
||||
|
||||
&.active, &:hover {
|
||||
color: $purple-300;
|
||||
box-shadow: 0px -0.25rem 0px $purple-300 inset;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: {
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
selectedTab: 'armoire',
|
||||
armoire: [],
|
||||
questInviteResponses: [],
|
||||
cron: [],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
if (this.expand) {
|
||||
this.retrieveUserHistory();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectTab (type) {
|
||||
this.selectedTab = type;
|
||||
},
|
||||
async toggleHistoryOpen () {
|
||||
this.expand = !this.expand;
|
||||
if (this.expand) {
|
||||
this.retrieveUserHistory();
|
||||
}
|
||||
},
|
||||
async retrieveUserHistory () {
|
||||
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
|
||||
this.armoire = history.armoire;
|
||||
this.questInviteResponses = history.questInviteResponses;
|
||||
this.cron = history.cron;
|
||||
},
|
||||
questInviteResponseText (response) {
|
||||
if (response === 'accept') {
|
||||
return 'Accepted';
|
||||
}
|
||||
if (response === 'reject') {
|
||||
return 'Rejected';
|
||||
}
|
||||
if (response === 'leave') {
|
||||
return 'Left active quest';
|
||||
}
|
||||
if (response === 'invite') {
|
||||
return 'Accepted as owner';
|
||||
}
|
||||
if (response === 'abort') {
|
||||
return 'Aborted by owner';
|
||||
}
|
||||
if (response === 'abortByLeader') {
|
||||
return 'Aborted by party leader';
|
||||
}
|
||||
if (response === 'cancel') {
|
||||
return 'Cancelled before start';
|
||||
}
|
||||
if (response === 'cancelByLeader') {
|
||||
return 'Cancelled before start by party leader';
|
||||
}
|
||||
return response;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
|
||||
<form
|
||||
@submit.prevent="saveHero({hero: {
|
||||
_id: hero._id,
|
||||
profile: hero.profile
|
||||
}, msg: 'Users Profile'})"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -8,6 +13,9 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
User Profile
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -51,13 +59,16 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -101,6 +112,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
<h3>{{ $t('footerCompany') }}</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/static/contact">
|
||||
<a href="mailto:admin@habitica.com">
|
||||
{{ $t('contactUs') }}
|
||||
</router-link>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/static/press-kit">
|
||||
@@ -55,9 +55,9 @@
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://habitica.fandom.com/wiki/Whats_New"
|
||||
target="_blank"
|
||||
>{{ $t('oldNews') }}
|
||||
@click="showBailey()"
|
||||
>
|
||||
{{ $t('oldNews') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -80,7 +80,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://habitica.fandom.com/wiki/Contributing_to_Habitica"
|
||||
href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica"
|
||||
target="_blank"
|
||||
>{{ $t('companyContribute') }}
|
||||
</a>
|
||||
@@ -131,13 +131,6 @@
|
||||
>{{ $t('requestFeature') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://habitica.fandom.com/"
|
||||
target="_blank"
|
||||
>{{ $t('wiki') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Developers -->
|
||||
@@ -165,13 +158,6 @@
|
||||
>{{ $t('guidanceForBlacksmiths') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations"
|
||||
target="_blank"
|
||||
>{{ $t('communityExtensions') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -212,12 +198,12 @@
|
||||
</a>
|
||||
<a
|
||||
class="social-circle"
|
||||
href="https://twitter.com/habitica/"
|
||||
href="https://bsky.app/profile/habitica.com"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="social-icon svg-icon twitter"
|
||||
v-html="icons.twitter"
|
||||
class="social-icon svg-icon bluesky"
|
||||
v-html="icons.bluesky"
|
||||
></div>
|
||||
</a>
|
||||
<a
|
||||
@@ -525,7 +511,7 @@ footer {
|
||||
background-color: $gray-500;
|
||||
color: $gray-50;
|
||||
padding: 32px 142px 40px;
|
||||
a {
|
||||
a, a:not([href]) {
|
||||
color: $gray-50;
|
||||
}
|
||||
a:hover {
|
||||
@@ -814,7 +800,7 @@ h3 {
|
||||
}
|
||||
}
|
||||
|
||||
.twitter svg {
|
||||
.bluesky svg {
|
||||
background-color: #e1e0e3;
|
||||
fill: #878190;
|
||||
height: 24px;
|
||||
@@ -853,7 +839,7 @@ import Vue from 'vue';
|
||||
|
||||
// images
|
||||
import melior from '@/assets/svg/melior.svg';
|
||||
import twitter from '@/assets/svg/twitter.svg';
|
||||
import bluesky from '@/assets/svg/bluesky.svg';
|
||||
import facebook from '@/assets/svg/facebook.svg';
|
||||
import instagram from '@/assets/svg/instagram.svg';
|
||||
import tumblr from '@/assets/svg/tumblr.svg';
|
||||
@@ -885,7 +871,7 @@ export default {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
melior,
|
||||
twitter,
|
||||
bluesky,
|
||||
facebook,
|
||||
instagram,
|
||||
tumblr,
|
||||
@@ -958,6 +944,7 @@ export default {
|
||||
},
|
||||
async jumpTime (amount) {
|
||||
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
|
||||
setTimeout(() => {
|
||||
if (amount > 0) {
|
||||
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
@@ -965,10 +952,12 @@ export default {
|
||||
}
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
}, 1000);
|
||||
},
|
||||
async resetTime () {
|
||||
const response = await axios.post('/api/v4/debug/jump-time', { reset: true });
|
||||
const time = new Date(response.data.data.time);
|
||||
setTimeout(() => {
|
||||
Vue.config.clock.restore();
|
||||
Vue.config.clock = sinon.useFakeTimers({
|
||||
now: time,
|
||||
@@ -976,6 +965,7 @@ export default {
|
||||
});
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
}, 1000);
|
||||
},
|
||||
addExp () {
|
||||
// @TODO: Name these variables better
|
||||
@@ -1003,7 +993,6 @@ export default {
|
||||
async bossRage () {
|
||||
await axios.post('/api/v4/debug/boss-rage');
|
||||
},
|
||||
|
||||
async makeAdmin () {
|
||||
await axios.post('/api/v4/debug/make-admin');
|
||||
// @TODO: Notification.text('You are now an admin!
|
||||
@@ -1013,6 +1002,9 @@ export default {
|
||||
donate () {
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
|
||||
},
|
||||
showBailey () {
|
||||
this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="member.preferences"
|
||||
class="avatar"
|
||||
:style="{width, height, paddingTop}"
|
||||
:class="backgroundClass"
|
||||
:class="topLevelClassList"
|
||||
@click.prevent="castEnd()"
|
||||
>
|
||||
<div
|
||||
@@ -55,7 +55,11 @@
|
||||
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('head'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
|
||||
<span :class="['hair_flower_' + member.preferences.hair.flower, specialMountClass]"></span>
|
||||
<span
|
||||
:class="[
|
||||
'hair_flower_' + member.preferences.hair.flower, specialMountClass
|
||||
]"
|
||||
></span>
|
||||
<span
|
||||
v-if="!hideGear('shield')"
|
||||
:class="[getGearClass('shield'), specialMountClass]"
|
||||
@@ -63,6 +67,7 @@
|
||||
<span
|
||||
v-if="!hideGear('weapon')"
|
||||
:class="[getGearClass('weapon'), specialMountClass]"
|
||||
class="weapon"
|
||||
></span>
|
||||
</template>
|
||||
<!-- Resting-->
|
||||
@@ -96,15 +101,23 @@
|
||||
|
||||
.avatar {
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
image-rendering: pixelated;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&.centered-avatar {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// resetting the additional padding
|
||||
margin-bottom: -0.5rem !important;
|
||||
}
|
||||
|
||||
.character-sprites {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.character-sprites span {
|
||||
@@ -123,22 +136,49 @@
|
||||
.invert {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.debug {
|
||||
border: 1px solid red;
|
||||
|
||||
.character-sprites {
|
||||
border: 1px solid blue;
|
||||
}
|
||||
|
||||
.weapon {
|
||||
border: 1px solid green;
|
||||
}
|
||||
|
||||
span {
|
||||
border: 1px solid yellow;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import some from 'lodash/some';
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import foolPet from '../mixins/foolPet';
|
||||
|
||||
import ClassBadge from '@/components/members/classBadge';
|
||||
|
||||
/**
|
||||
* TODO replace avatarOnly with multiple options like
|
||||
* - showMount
|
||||
* - showPet
|
||||
* - showBackground
|
||||
* - showWeapons
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClassBadge,
|
||||
},
|
||||
mixins: [foolPet],
|
||||
props: {
|
||||
debugMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -156,14 +196,21 @@ export default {
|
||||
},
|
||||
overrideAvatarGear: {
|
||||
type: Object,
|
||||
default (data) {
|
||||
return data;
|
||||
},
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 140,
|
||||
type: String,
|
||||
default: '141px',
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 147,
|
||||
type: String,
|
||||
default: '147px',
|
||||
},
|
||||
centerAvatar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
spritesMargin: {
|
||||
type: String,
|
||||
@@ -171,11 +218,16 @@ export default {
|
||||
},
|
||||
overrideTopPadding: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
showVisualBuffs: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showWeapon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
@@ -204,6 +256,19 @@ export default {
|
||||
|
||||
return val;
|
||||
},
|
||||
topLevelClassList () {
|
||||
const classes = [this.backgroundClass];
|
||||
|
||||
if (this.debugMode) {
|
||||
classes.push('debug');
|
||||
}
|
||||
|
||||
if (this.centerAvatar) {
|
||||
classes.push('centered-avatar');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
},
|
||||
backgroundClass () {
|
||||
if (this.member) {
|
||||
const { background } = this.member.preferences;
|
||||
@@ -256,11 +321,10 @@ export default {
|
||||
return null;
|
||||
},
|
||||
petClass () {
|
||||
if (some(
|
||||
this.currentEventList,
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
|
||||
)) {
|
||||
return this.foolPet(this.member.items.currentPet);
|
||||
const foolEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.aprilFools);
|
||||
if (foolEvent) {
|
||||
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
|
||||
}
|
||||
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
|
||||
return '';
|
||||
@@ -290,6 +354,10 @@ export default {
|
||||
},
|
||||
hideGear (gearType) {
|
||||
if (!this.member) return true;
|
||||
if (!this.showWeapon) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (gearType === 'weapon') {
|
||||
const equippedWeapon = this.member.items.gear[this.costumeClass][gearType];
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
>
|
||||
<div
|
||||
v-for="option in items"
|
||||
:key="option.key"
|
||||
:id="option.imageName"
|
||||
:key="option.key"
|
||||
class="outer-option-background"
|
||||
:class="{
|
||||
premium: Boolean(option.gem),
|
||||
@@ -28,7 +28,6 @@
|
||||
v-if="!option.none"
|
||||
class="sprite"
|
||||
:prefix="option.isGear ? 'shop' : 'icon'"
|
||||
:imageName="option.imageName"
|
||||
:image-name="option.imageName"
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="isUserMentioned"
|
||||
class="mentioned-icon"
|
||||
></div>
|
||||
<div
|
||||
v-if="hasPermission(user, 'moderator') && msg.flagCount"
|
||||
class="message-hidden"
|
||||
>
|
||||
{{ flagCountDescription }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<user-link
|
||||
:user-id="msg.uuid"
|
||||
:name="msg.user"
|
||||
:backer="msg.backer"
|
||||
:contributor="msg.contributor"
|
||||
/>
|
||||
<p class="time">
|
||||
<span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>@{{ msg.username }}</span>
|
||||
<span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>•</span>
|
||||
<span
|
||||
v-b-tooltip.hover="messageDate"
|
||||
>{{ msg.timestamp | timeAgo }} </span>
|
||||
<span v-if="msg.client && user.contributor.level >= 4">({{ msg.client }})</span>
|
||||
</p>
|
||||
<div
|
||||
ref="markdownContainer"
|
||||
class="text markdown"
|
||||
dir="auto"
|
||||
v-html="parseMarkdown(msg.text)"
|
||||
></div>
|
||||
<hr>
|
||||
<div
|
||||
v-if="msg.id"
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="action d-flex align-items-center"
|
||||
@click="copyAsTodo(msg)"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.copy"
|
||||
></div>
|
||||
<div>{{ $t('copyAsTodo') }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="(user.flags.communityGuidelinesAccepted && msg.uuid !== 'system')
|
||||
&& (!isMessageReported || hasPermission(user, 'moderator'))"
|
||||
class="action d-flex align-items-center"
|
||||
@click="report(msg)"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.report"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('report') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.uuid === user._id || hasPermission(user, 'moderator')"
|
||||
class="action d-flex align-items-center"
|
||||
@click="remove()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.delete"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('delete') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-b-tooltip="{title: likeTooltip(msg.likes[user._id])}"
|
||||
class="ml-auto d-flex"
|
||||
>
|
||||
<div
|
||||
v-if="likeCount > 0"
|
||||
class="action d-flex align-items-center mr-0"
|
||||
:class="{activeLike: msg.likes[user._id]}"
|
||||
@click="like()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
:title="$t('liked')"
|
||||
v-html="icons.liked"
|
||||
></div>
|
||||
+{{ likeCount }}
|
||||
</div>
|
||||
<div
|
||||
v-if="likeCount === 0"
|
||||
class="action d-flex align-items-center mr-0"
|
||||
:class="{activeLike: msg.likes[user._id]}"
|
||||
@click="like()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
:title="$t('like')"
|
||||
v-html="icons.like"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="!msg.likes[user._id]">{{ $t('like') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.at-highlight {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
padding: 0.1rem;
|
||||
}
|
||||
|
||||
.at-text {
|
||||
color: #6133b4;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.mentioned-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #bda8ff;
|
||||
box-shadow: 0 1px 1px 0 rgba(26, 24, 29, 0.12);
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
.message-hidden {
|
||||
margin-left: 1.5em;
|
||||
margin-top: 1em;
|
||||
color: red;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #878190;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
color: #4e4a57;
|
||||
text-align: initial;
|
||||
min-height: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
color: #878190;
|
||||
margin-right: 1em;
|
||||
font-size: 12px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
color: #A5A1AC;
|
||||
margin-right: .2em;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.activeLike {
|
||||
color: $purple-300;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
|
||||
import { CHAT_FLAG_LIMIT_FOR_HIDING, CHAT_FLAG_FROM_SHADOW_MUTE } from '@/../../common/script/constants';
|
||||
import renderWithMentions from '@/libs/renderWithMentions';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import userLink from '../userLink';
|
||||
|
||||
import deleteIcon from '@/assets/svg/delete.svg';
|
||||
import copyIcon from '@/assets/svg/copy.svg';
|
||||
import likeIcon from '@/assets/svg/like.svg';
|
||||
import likedIcon from '@/assets/svg/liked.svg';
|
||||
import reportIcon from '@/assets/svg/report.svg';
|
||||
|
||||
export default {
|
||||
components: { userLink },
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
date (value) {
|
||||
// @TODO: Vue doesn't support this so we cant user preference
|
||||
return moment(value).toDate().toString();
|
||||
},
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: {
|
||||
msg: {},
|
||||
groupId: {},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
like: likeIcon,
|
||||
copy: copyIcon,
|
||||
report: reportIcon,
|
||||
delete: deleteIcon,
|
||||
liked: likedIcon,
|
||||
}),
|
||||
reported: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isUserMentioned () {
|
||||
const message = this.msg;
|
||||
|
||||
if (message.highlight) return true;
|
||||
|
||||
const { user } = this;
|
||||
const displayName = user.profile.name;
|
||||
const { username } = user.auth.local;
|
||||
const pattern = `@(${escapeRegExp(displayName)}|${escapeRegExp(username)})(\\b)`;
|
||||
message.highlight = new RegExp(pattern, 'i').test(message.text);
|
||||
|
||||
return message.highlight;
|
||||
},
|
||||
likeCount () {
|
||||
const message = this.msg;
|
||||
if (!message.likes) return 0;
|
||||
|
||||
let likeCount = 0;
|
||||
for (const key of Object.keys(message.likes)) {
|
||||
const like = message.likes[key];
|
||||
if (like) likeCount += 1;
|
||||
}
|
||||
return likeCount;
|
||||
},
|
||||
isMessageReported () {
|
||||
return (this.msg.flags && this.msg.flags[this.user.id]) || this.reported;
|
||||
},
|
||||
flagCountDescription () {
|
||||
if (!this.msg.flagCount) return '';
|
||||
if (this.msg.flagCount < CHAT_FLAG_LIMIT_FOR_HIDING) return 'Message flagged once, not hidden';
|
||||
if (this.msg.flagCount < CHAT_FLAG_FROM_SHADOW_MUTE) return 'Message hidden';
|
||||
return 'Message hidden (shadow-muted)';
|
||||
},
|
||||
messageDate () {
|
||||
const date = moment(this.msg.timestamp).toDate();
|
||||
return date.toString();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
const links = this.$refs.markdownContainer.getElementsByTagName('a');
|
||||
for (let i = 0; i < links.length; i += 1) {
|
||||
let link = links[i].pathname;
|
||||
|
||||
// Internet Explorer does not provide the leading slash character in the pathname
|
||||
link = link.charAt(0) === '/' ? link : `/${link}`;
|
||||
|
||||
if (link.startsWith('/profile/')) {
|
||||
links[i].onclick = ev => {
|
||||
ev.preventDefault();
|
||||
this.$router.push({ path: link });
|
||||
};
|
||||
}
|
||||
}
|
||||
this.CHAT_FLAG_LIMIT_FOR_HIDING = CHAT_FLAG_LIMIT_FOR_HIDING;
|
||||
this.CHAT_FLAG_FROM_SHADOW_MUTE = CHAT_FLAG_FROM_SHADOW_MUTE;
|
||||
this.$emit('chat-card-mounted', this.msg.id);
|
||||
},
|
||||
methods: {
|
||||
async like () {
|
||||
const message = cloneDeep(this.msg);
|
||||
|
||||
await this.$store.dispatch('chat:like', {
|
||||
groupId: this.groupId,
|
||||
chatId: message.id,
|
||||
});
|
||||
|
||||
message.likes[this.user._id] = !message.likes[this.user._id];
|
||||
|
||||
this.$emit('message-liked', message);
|
||||
this.$root.$emit('bv::hide::tooltip');
|
||||
},
|
||||
likeTooltip (likedStatus) {
|
||||
if (!likedStatus) return this.$t('like');
|
||||
return null;
|
||||
},
|
||||
copyAsTodo (message) {
|
||||
this.$root.$emit('habitica::copy-as-todo', message);
|
||||
},
|
||||
report () {
|
||||
this.$root.$on('habitica:report-result', data => {
|
||||
if (data.ok) {
|
||||
this.reported = true;
|
||||
}
|
||||
|
||||
this.$root.$off('habitica:report-result');
|
||||
});
|
||||
|
||||
this.$root.$emit('habitica::report-chat', {
|
||||
message: this.msg,
|
||||
groupId: this.groupId || 'privateMessage',
|
||||
});
|
||||
},
|
||||
async remove () {
|
||||
if (!window.confirm(this.$t('areYouSureDeleteMessage'))) return; // eslint-disable-line no-alert
|
||||
|
||||
const message = this.msg;
|
||||
this.$emit('message-removed', message);
|
||||
|
||||
await this.$store.dispatch('chat:deleteChat', {
|
||||
groupId: this.groupId,
|
||||
chatId: message.id,
|
||||
});
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
return renderWithMentions(text, this.user);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -3,15 +3,6 @@
|
||||
ref="container"
|
||||
class="container-fluid"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<copy-as-todo-modal
|
||||
:group-type="groupType"
|
||||
:group-name="groupName"
|
||||
:group-id="groupId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row loadmore">
|
||||
<div v-if="canLoadMore">
|
||||
<div class="loadmore-divider"></div>
|
||||
@@ -33,11 +24,14 @@
|
||||
<div
|
||||
v-for="msg in messages.filter(m => chat && canViewFlag(m))"
|
||||
:key="msg.id"
|
||||
class="message-row"
|
||||
:class="{ 'margin-right': user._id !== msg.uuid}"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<avatar
|
||||
v-if="user._id !== msg.uuid && msg.uuid !== 'system'"
|
||||
class="avatar-left"
|
||||
:height="null"
|
||||
:class="{ invisible: avatarUnavailable(msg) }"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
|
||||
:avatar-only="true"
|
||||
@@ -45,20 +39,19 @@
|
||||
:override-top-padding="'14px'"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
<div class="card">
|
||||
<chat-card
|
||||
<message-card
|
||||
:msg="msg"
|
||||
:group-id="groupId"
|
||||
:user-sent-message="user._id === msg.uuid"
|
||||
@message-liked="messageLiked"
|
||||
@message-removed="messageRemoved"
|
||||
@show-member-modal="showMemberModal"
|
||||
@chat-card-mounted="itemWasMounted"
|
||||
@message-card-mounted="itemWasMounted"
|
||||
/>
|
||||
</div>
|
||||
<avatar
|
||||
v-if="user._id === msg.uuid"
|
||||
:class="{ invisible: avatarUnavailable(msg) }"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
|
||||
:height="null"
|
||||
:avatar-only="true"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'14px'"
|
||||
@@ -105,11 +98,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-left {
|
||||
margin-left: -1.5rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
@@ -137,11 +125,27 @@
|
||||
margin-bottom: .5em;
|
||||
padding: 0rem;
|
||||
width: 90%;
|
||||
|
||||
&.system-message {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.message-scroll .d-flex {
|
||||
min-width: 1px;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
margin-left: 12px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1.2rem;
|
||||
|
||||
&:not(.margin-right) {
|
||||
.d-flex {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -152,13 +156,13 @@ import findIndex from 'lodash/findIndex';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
import Avatar from '../avatar';
|
||||
import copyAsTodoModal from './copyAsTodoModal';
|
||||
import chatCard from './chatCard';
|
||||
import MessageCard from '@/components/messages/messageCard.vue';
|
||||
|
||||
// TODO merge chatMessages.vue (party message list) with messageList.vue (private message list)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
copyAsTodoModal,
|
||||
chatCard,
|
||||
MessageCard,
|
||||
Avatar,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="copyAsTodo"
|
||||
:title="$t('copyMessageAsToDo')"
|
||||
:hide-footer="true"
|
||||
size="md"
|
||||
>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="task.text"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<textarea
|
||||
v-model="task.notes"
|
||||
class="form-control"
|
||||
rows="5"
|
||||
focus-element="true"
|
||||
></textarea>
|
||||
</div>
|
||||
<hr>
|
||||
<task
|
||||
v-if="task._id"
|
||||
:is-user="isUser"
|
||||
:task="task"
|
||||
/>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('close') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="saveTodo()"
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
||||
import { mapActions } from '@/libs/store';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
import Task from '@/components/tasks/task';
|
||||
|
||||
const baseUrl = 'https://habitica.com';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
Task,
|
||||
},
|
||||
mixins: [notificationsMixin],
|
||||
props: ['copyingMessage', 'groupType', 'groupName', 'groupId'],
|
||||
data () {
|
||||
return {
|
||||
isUser: true,
|
||||
task: {},
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica::copy-as-todo', message => {
|
||||
const notes = `${message.user || 'system message'}${message.user ? ' wrote' : ''} in [${this.groupName}](${this.groupPath()})`;
|
||||
const newTask = {
|
||||
text: message.text,
|
||||
type: 'todo',
|
||||
notes,
|
||||
};
|
||||
this.task = taskDefaults(newTask, this.$store.state.user.data);
|
||||
this.$root.$emit('bv::show::modal', 'copyAsTodo');
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica::copy-as-todo');
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
createTask: 'tasks:create',
|
||||
}),
|
||||
groupPath () {
|
||||
if (this.groupType === 'party') {
|
||||
return `${baseUrl}/party`;
|
||||
}
|
||||
return `${baseUrl}/groups/guild/${this.groupId}`;
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'copyAsTodo');
|
||||
},
|
||||
saveTodo () {
|
||||
this.createTask(this.task);
|
||||
this.text(this.$t('messageAddedAsToDo'));
|
||||
this.$root.$emit('bv::hide::modal', 'copyAsTodo');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -22,13 +22,13 @@
|
||||
:placeholder="placeholder"
|
||||
:class="{'user-entry': newMessage}"
|
||||
:maxlength="MAX_MESSAGE_LENGTH"
|
||||
@keydown="updateCarretPosition"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keyup.ctrl.enter="sendMessageShortcut()"
|
||||
@keydown.tab="handleTab($event)"
|
||||
@keydown.up="selectPreviousAutocomplete($event)"
|
||||
@keydown.down="selectNextAutocomplete($event)"
|
||||
@keypress.enter="selectAutocomplete($event)"
|
||||
@keydown.esc="handleEscape($event)"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
@paste="disableMessageSendShortcut()"
|
||||
></textarea>
|
||||
<span>{{ currentLength }} / {{ MAX_MESSAGE_LENGTH }}</span>
|
||||
@@ -36,8 +36,8 @@
|
||||
ref="autocomplete"
|
||||
:text="newMessage"
|
||||
:textbox="textbox"
|
||||
:coords="coords"
|
||||
:caret-position="caretPosition"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
:chat="group.chat"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
@@ -74,7 +74,7 @@
|
||||
<slot name="additionRow"></slot>
|
||||
<div class="row">
|
||||
<div class="hr col-12"></div>
|
||||
<chat-message
|
||||
<chat-messages
|
||||
:chat.sync="group.chat"
|
||||
:group-type="group.type"
|
||||
:group-id="group._id"
|
||||
@@ -86,16 +86,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants';
|
||||
import externalLinks from '../../mixins/externalLinks';
|
||||
|
||||
import autocomplete from '../chat/autoComplete';
|
||||
import communityGuidelines from './communityGuidelines';
|
||||
import chatMessage from '../chat/chatMessages';
|
||||
import chatMessages from '../chat/chatMessages';
|
||||
import { mapState } from '@/libs/store';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
@@ -104,23 +103,18 @@ export default {
|
||||
components: {
|
||||
autocomplete,
|
||||
communityGuidelines,
|
||||
chatMessage,
|
||||
chatMessages,
|
||||
},
|
||||
mixins: [externalLinks],
|
||||
mixins: [externalLinks, autoCompleteHelperMixin],
|
||||
props: ['label', 'group', 'placeholder'],
|
||||
data () {
|
||||
return {
|
||||
newMessage: '',
|
||||
sending: false,
|
||||
caretPosition: 0,
|
||||
chat: {
|
||||
submitDisable: false,
|
||||
submitTimeout: null,
|
||||
},
|
||||
coords: {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
},
|
||||
textbox: null,
|
||||
MAX_MESSAGE_LENGTH: MAX_MESSAGE_LENGTH.toString(),
|
||||
};
|
||||
@@ -142,35 +136,6 @@ export default {
|
||||
this.handleExternalLinks();
|
||||
},
|
||||
methods: {
|
||||
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
|
||||
getCoord (e, text) {
|
||||
this.caretPosition = text.selectionEnd;
|
||||
const div = document.createElement('div');
|
||||
const span = document.createElement('span');
|
||||
const copyStyle = getComputedStyle(text);
|
||||
|
||||
[].forEach.call(copyStyle, prop => {
|
||||
div.style[prop] = copyStyle[prop];
|
||||
});
|
||||
|
||||
div.style.position = 'absolute';
|
||||
document.body.appendChild(div);
|
||||
div.textContent = text.value.substr(0, this.caretPosition);
|
||||
span.textContent = text.value.substr(this.caretPosition) || '.';
|
||||
div.appendChild(span);
|
||||
this.coords = {
|
||||
TOP: span.offsetTop,
|
||||
LEFT: span.offsetLeft,
|
||||
};
|
||||
document.body.removeChild(div);
|
||||
},
|
||||
updateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
|
||||
this._updateCarretPosition(eventUpdate);
|
||||
}, 250),
|
||||
_updateCarretPosition (eventUpdate) {
|
||||
const text = eventUpdate.target;
|
||||
this.getCoord(eventUpdate, text);
|
||||
},
|
||||
async sendMessageShortcut () {
|
||||
// If the user recently pasted in the text field, don't submit
|
||||
if (!this.chat.submitDisable) {
|
||||
@@ -221,50 +186,6 @@ export default {
|
||||
}, 500);
|
||||
},
|
||||
|
||||
handleTab (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
} else {
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleEscape (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
selectNextAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
},
|
||||
|
||||
selectPreviousAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
}
|
||||
},
|
||||
|
||||
selectAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
if (this.$refs.autocomplete.selected !== null) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.makeSelection();
|
||||
} else {
|
||||
// no autocomplete selected, newline instead
|
||||
this.$refs.autocomplete.cancel();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
this.newMessage = newText;
|
||||
// Wait for v-modal to update
|
||||
@@ -273,7 +194,6 @@ export default {
|
||||
this.textbox.focus();
|
||||
});
|
||||
},
|
||||
|
||||
fetchRecentMessages () {
|
||||
this.$emit('fetchRecentMessages');
|
||||
},
|
||||
@@ -284,10 +204,7 @@ export default {
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
// Reset chat
|
||||
this.newMessage = '';
|
||||
this.coords = {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
};
|
||||
this.autoCompleteMixinResetCoordsPosition();
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
@@ -225,10 +225,9 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="quest-icon">
|
||||
<div
|
||||
<Sprite
|
||||
class="quest"
|
||||
:class="`inventory_quest_scroll_${questData.key}`"
|
||||
></div>
|
||||
:image-name="`inventory_quest_scroll_${questData.key}`" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
<div class="topbar-dropdown">
|
||||
<router-link
|
||||
v-if="user.permissions.fullAccess ||
|
||||
user.permissions.userSupport || user.permissions.newsPoster"
|
||||
user.permissions.userSupport"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'adminPanel'}"
|
||||
>
|
||||
@@ -334,11 +334,6 @@
|
||||
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/Habitica_Wiki"
|
||||
target="_blank"
|
||||
>{{ $t('wiki') }}</a>
|
||||
</div>
|
||||
</li>
|
||||
</b-navbar-nav>
|
||||
|
||||
@@ -12,20 +12,21 @@
|
||||
<strong> {{ notification.data.title }} </strong>
|
||||
<span> {{ notification.data.text }} </span>
|
||||
</div>
|
||||
<div
|
||||
<Sprite
|
||||
slot="icon"
|
||||
class="mt-3"
|
||||
:class="notification.data.icon"
|
||||
></div>
|
||||
:image-name="notification.data.icon" />
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
Sprite,
|
||||
},
|
||||
props: {
|
||||
notification: {
|
||||
@@ -41,7 +42,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
if (!this.notification || !this.notification.data) {
|
||||
if (!this.notification || !this.notification.data
|
||||
|| this.notification.data.destination === this.$route.path) {
|
||||
return;
|
||||
}
|
||||
if (this.notification.data.destination.indexOf('backgrounds') !== -1) {
|
||||
|
||||
@@ -10,20 +10,21 @@
|
||||
slot="content"
|
||||
v-html="$t('newSubscriberItem')"
|
||||
></div>
|
||||
<div
|
||||
<Sprite
|
||||
slot="icon"
|
||||
:class="mysteryClass"
|
||||
></div>
|
||||
:image-name="mysteryClass" />
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import BaseNotification from './base';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
Sprite,
|
||||
},
|
||||
props: ['notification', 'canRemove'],
|
||||
computed: {
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
</div>
|
||||
<div slot="drawer-header">
|
||||
<div class="drawer-tab-container">
|
||||
<div class="clearfix">
|
||||
<div class="clearfix mb-2">
|
||||
<toggle-switch
|
||||
class="float-right align-with-tab"
|
||||
:label="$t(costumeMode ? 'useCostume' : 'autoEquipBattleGear')"
|
||||
@@ -410,7 +410,8 @@ export default {
|
||||
const ownedItem = this.flatGear[gearKey];
|
||||
|
||||
const isSearched = !searchText
|
||||
|| ownedItem.text().toLowerCase().indexOf(searchText) !== -1;
|
||||
|| ownedItem.text().toLowerCase().indexOf(searchText) !== -1
|
||||
|| ownedItem.notes().toLowerCase().indexOf(searchText) !== -1;
|
||||
|
||||
if (ownedItem.klass !== 'base' && isSearched) {
|
||||
const { type } = ownedItem;
|
||||
|
||||
@@ -338,9 +338,10 @@ export default {
|
||||
if (itemQuantity > 0 && isAllowed) {
|
||||
const item = contentItems[itemKey];
|
||||
|
||||
const isSearched = !searchText || item.text()
|
||||
.toLowerCase()
|
||||
.indexOf(searchText) !== -1;
|
||||
const isSearched = !searchText
|
||||
|| item.text().toLowerCase().indexOf(searchText) !== -1
|
||||
|| item.notes().toLowerCase().indexOf(searchText) !== -1;
|
||||
|
||||
if (isSearched && item) {
|
||||
itemsArray.push({
|
||||
...item,
|
||||
|
||||
@@ -627,6 +627,8 @@ export default {
|
||||
animals.push({
|
||||
key: specialKey,
|
||||
eggKey,
|
||||
eggName: text(),
|
||||
mountName: text(),
|
||||
potionKey,
|
||||
name: text(),
|
||||
canFind,
|
||||
@@ -657,7 +659,6 @@ export default {
|
||||
}
|
||||
|
||||
this.cachedAnimalList[key] = animals;
|
||||
|
||||
return animals;
|
||||
},
|
||||
listAnimals (animalGroup, type, hideMissing, sort, searchText) {
|
||||
@@ -670,7 +671,9 @@ export default {
|
||||
}
|
||||
|
||||
if (searchText && searchText !== '') {
|
||||
animals = _filter(animals, a => a.name.toLowerCase().indexOf(searchText) !== -1);
|
||||
animals = _filter(animals, a => a.name.toLowerCase().indexOf(searchText) !== -1
|
||||
|| a.eggName.toLowerCase().indexOf(searchText) !== -1
|
||||
|| a.mountName.toLowerCase().indexOf(searchText) !== -1);
|
||||
}
|
||||
|
||||
// 2. Sort
|
||||
@@ -693,7 +696,6 @@ export default {
|
||||
}
|
||||
|
||||
this.viewOptions[animalGroup.key].animalCount = animals.length;
|
||||
|
||||
return animals;
|
||||
},
|
||||
countOwnedAnimals (animalGroup, type) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="pet-background">
|
||||
<Sprite
|
||||
class="mount"
|
||||
:image-name="`Mount_Icon_${mount.key}`"
|
||||
:image-name="`stable_Mount_Icon_${mount.key}`"
|
||||
/>
|
||||
</div>
|
||||
<h4 class="title">
|
||||
@@ -66,6 +66,7 @@
|
||||
|
||||
.mount {
|
||||
margin: 0 auto;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
|
||||
@@ -114,7 +114,6 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import some from 'lodash/some';
|
||||
import moment from 'moment';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { mapState } from '@/libs/store';
|
||||
@@ -183,13 +182,12 @@ export default {
|
||||
return 'GreyedOut';
|
||||
},
|
||||
imageName () {
|
||||
if (this.isOwned() && some(
|
||||
this.currentEventList,
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
|
||||
)) {
|
||||
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key)}`;
|
||||
const foolEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.aprilFools);
|
||||
if (this.isOwned() && foolEvent) {
|
||||
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key, foolEvent.aprilFools)}`;
|
||||
const petString = `${this.item.eggKey}-${this.item.key}`;
|
||||
return `stable_${this.foolPet(petString)}`;
|
||||
return `stable_${this.foolPet(petString, foolEvent.aprilFools)}`;
|
||||
}
|
||||
|
||||
if (this.isOwned() || (this.mountOwned() && this.isHatchable())) {
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
:name="member.profile.name"
|
||||
:backer="member.backer"
|
||||
:contributor="member.contributor"
|
||||
:smaller-style="true"
|
||||
/>
|
||||
<inline-class-badge
|
||||
v-if="member.stats"
|
||||
|
||||
114
website/client/src/components/messages/likeButton.vue
Normal file
114
website/client/src/components/messages/likeButton.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-inline-flex like-button"
|
||||
@click="like()"
|
||||
>
|
||||
<div
|
||||
v-b-tooltip="{title: likeTooltip(likeCount)}"
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
v-if="likeCount > 0"
|
||||
class="action d-flex align-items-center mr-0"
|
||||
:class="{isLiked: true, currentUserLiked: likedByCurrentUser}"
|
||||
>
|
||||
<div
|
||||
class="svg-icon mr-1"
|
||||
:title="$t('liked')"
|
||||
v-html="likedIcon"
|
||||
></div>
|
||||
+{{ likeCount }}
|
||||
</div>
|
||||
<div
|
||||
v-if="likeCount === 0"
|
||||
class="action d-flex align-items-center mr-1"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
:title="$t('like')"
|
||||
v-html="icons.like"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="likeCount === 0">{{ $t('like') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/tiers.scss';
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
|
||||
.svg-icon {
|
||||
color: $gray-100;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&.isLiked.currentUserLiked {
|
||||
color: $purple-200;
|
||||
font-weight: bold;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.like-button {
|
||||
color: $gray-100;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: $purple-200;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import likeIcon from '@/assets/svg/like.svg';
|
||||
import likedIcon from '@/assets/svg/liked.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
likeCount: {
|
||||
type: Number,
|
||||
},
|
||||
likedByCurrentUser: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
like: likeIcon,
|
||||
liked: likedIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
likedIcon () {
|
||||
return this.likedByCurrentUser ? this.icons.liked : this.icons.like;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async like () {
|
||||
this.$emit('toggle-like');
|
||||
},
|
||||
likeTooltip (likedStatus) {
|
||||
if (!likedStatus) return this.$t('like');
|
||||
return null;
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,12 +1,44 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<div
|
||||
class="card"
|
||||
:class="{
|
||||
'system-message': isSystemMessage
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-b-tooltip.hover="messageDateForSystemMessage"
|
||||
class="message-card"
|
||||
|
||||
:class="{
|
||||
'user-sent-message': userSentMessage,
|
||||
'user-received-message': !userSentMessage && !isSystemMessage,
|
||||
'system-message': isSystemMessage
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="isUserMentioned"
|
||||
class="mentioned-icon"
|
||||
></div>
|
||||
<div
|
||||
v-if="userIsModerator && msg.flagCount"
|
||||
class="message-hidden"
|
||||
>
|
||||
{{ flagCountDescription }}
|
||||
</div>
|
||||
<div
|
||||
class="card-body"
|
||||
>
|
||||
<user-link
|
||||
v-if="!isSystemMessage"
|
||||
:user-id="msg.uuid"
|
||||
:name="msg.user"
|
||||
:backer="msg.backer"
|
||||
:contributor="msg.contributor"
|
||||
/>
|
||||
<p class="time">
|
||||
<p
|
||||
v-if="!isSystemMessage"
|
||||
class="time"
|
||||
>
|
||||
<span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
@@ -14,12 +46,86 @@
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>•</span>
|
||||
<span
|
||||
v-b-tooltip.hover="messageDate"
|
||||
>{{ msg.timestamp | timeAgo }} </span>
|
||||
<span v-if="msg.client && user.contributor.level >= 4"> ({{ msg.client }})</span>
|
||||
<span v-b-tooltip.hover="messageDate">{{ msg.timestamp | timeAgo }} </span>
|
||||
<span v-if="msg.client && user.contributor.level >= 4">
|
||||
({{ msg.client }})
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<b-dropdown
|
||||
v-if="!isSystemMessage"
|
||||
right="right"
|
||||
variant="flat"
|
||||
toggle-class="with-icon"
|
||||
class="card-menu no-min-width"
|
||||
:no-caret="true"
|
||||
>
|
||||
<template #button-content>
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon inline menuIcon color"
|
||||
v-html="icons.menuIcon"
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
<b-dropdown-item
|
||||
class="selectListItem"
|
||||
@click="copy(msg)"
|
||||
>
|
||||
<span class="with-icon">
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon icon-16 color"
|
||||
v-html="icons.copy"
|
||||
></span>
|
||||
<span v-once>
|
||||
{{ $t('copy') }}
|
||||
</span>
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="canReportMessage"
|
||||
class="selectListItem custom-hover--red"
|
||||
@click="report(msg)"
|
||||
>
|
||||
<span class="with-icon">
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon icon-16 color"
|
||||
v-html="icons.report"
|
||||
></span>
|
||||
<span v-once>
|
||||
{{ $t('report') }}
|
||||
</span>
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="canDeleteMessage"
|
||||
class="selectListItem custom-hover--red"
|
||||
@click="remove()"
|
||||
>
|
||||
<span class="with-icon">
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon icon-16 color"
|
||||
v-html="icons.delete"
|
||||
></span>
|
||||
<span v-once>
|
||||
{{ $t('delete') }}
|
||||
</span>
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<div
|
||||
v-if="isSystemMessage"
|
||||
class="system-message-body"
|
||||
>
|
||||
{{ msg.unformattedText }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
ref="markdownContainer"
|
||||
class="text markdown"
|
||||
dir="auto"
|
||||
v-html="parseMarkdown(msg.text)"
|
||||
@@ -31,43 +137,21 @@
|
||||
<span v-once>{{ $t('reportedMessage') }}</span><br>
|
||||
<span v-once>{{ $t('canDeleteNow') }}</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div
|
||||
v-if="msg.id"
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
v-if="!isMessageReported"
|
||||
class="action d-flex align-items-center"
|
||||
@click="report(msg)"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.report"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('report') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="action d-flex align-items-center"
|
||||
@click="remove()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.delete"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('delete') }}
|
||||
</div>
|
||||
|
||||
<like-button
|
||||
v-if="canLikeMessage"
|
||||
class="mt-75"
|
||||
:liked-by-current-user="msg.likes[user._id]"
|
||||
:like-count="likeCount"
|
||||
@toggle-like="like()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.message-card {
|
||||
.at-highlight {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
padding: 0.1rem;
|
||||
@@ -76,27 +160,50 @@
|
||||
.at-text {
|
||||
color: #6133b4;
|
||||
}
|
||||
|
||||
.card-menu button {
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.markdown p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/tiers.scss';
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
color: $gray-200;
|
||||
margin-right: 1em;
|
||||
font-size: 12px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
.card {
|
||||
background: transparent !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
color: $gray-300;
|
||||
margin-right: .2em;
|
||||
.message-card:not(.system-message) {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.mentioned-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: $purple-500;
|
||||
box-shadow: 0 1px 1px 0 rgba(26, 24, 29, 0.12);
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
.message-hidden {
|
||||
margin-left: 1.5em;
|
||||
margin-top: 1em;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.active {
|
||||
@@ -107,12 +214,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.message-card {
|
||||
border-radius: 7px;
|
||||
margin: 0;
|
||||
padding: 1rem 0.75rem 0.5rem 1rem;
|
||||
|
||||
&.system-message {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: $gray-200;
|
||||
color: $gray-100;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -123,6 +240,23 @@
|
||||
min-height: 0rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
&:not(.show) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body:hover {
|
||||
.card-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -133,39 +267,146 @@
|
||||
margin-top: 18px;
|
||||
color: $red-50;
|
||||
}
|
||||
|
||||
.selectListItem:not(:hover) .svg-icon.icon-16.color {
|
||||
color: #{$gray-100}
|
||||
}
|
||||
|
||||
.custom-hover--red {
|
||||
--hover-color: #{$maroon-50};
|
||||
--hover-background: #{rgba($red-500, 0.25)};
|
||||
}
|
||||
|
||||
.user-sent-message {
|
||||
border: 1px solid $purple-400;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
border: 1px solid $purple-400;
|
||||
}
|
||||
|
||||
.user-received-message {
|
||||
border: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.card-menu {
|
||||
// icon-color is the menu icon itself
|
||||
--icon-color: #{$gray-100};
|
||||
|
||||
--dropdown-item-hover-icon-color: #{$gray-100};
|
||||
|
||||
&:hover {
|
||||
--icon-color: #{$purple-300};
|
||||
}
|
||||
}
|
||||
|
||||
.menuIcon {
|
||||
width: 4px;
|
||||
height: 1rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.system-message-body {
|
||||
line-height: 1.71;
|
||||
text-align: center;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import { CHAT_FLAG_FROM_SHADOW_MUTE, CHAT_FLAG_LIMIT_FOR_HIDING } from '@/../../common/script/constants';
|
||||
import externalLinks from '../../mixins/externalLinks';
|
||||
|
||||
import { CopyToClipboardMixin } from '@/mixins/copyToClipboard';
|
||||
|
||||
import renderWithMentions from '@/libs/renderWithMentions';
|
||||
import { mapState } from '@/libs/store';
|
||||
import userLink from '../userLink';
|
||||
|
||||
import deleteIcon from '@/assets/svg/delete.svg';
|
||||
import reportIcon from '@/assets/svg/report.svg';
|
||||
import menuIcon from '@/assets/svg/menu.svg';
|
||||
import { userStateMixin } from '@/mixins/userState';
|
||||
import copyIcon from '@/assets/svg/copy.svg';
|
||||
import LikeButton from '@/components/messages/likeButton.vue';
|
||||
|
||||
const LikeLogicMixin = {
|
||||
computed: {
|
||||
likeCount () {
|
||||
const message = this.msg;
|
||||
if (!message.likes) return 0;
|
||||
|
||||
let likeCount = 0;
|
||||
for (const key of Object.keys(message.likes)) {
|
||||
const like = message.likes[key];
|
||||
if (like) likeCount += 1;
|
||||
}
|
||||
return likeCount;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
async like () {
|
||||
const message = cloneDeep(this.msg);
|
||||
|
||||
await this.$store.dispatch('chat:like', {
|
||||
groupId: this.groupId,
|
||||
chatMessageId: this.privateMessageMode ? message.uniqueMessageId : message.id,
|
||||
});
|
||||
|
||||
message.likes[this.user._id] = !message.likes[this.user._id];
|
||||
|
||||
this.$emit('message-liked', message);
|
||||
this.$root.$emit('bv::hide::tooltip');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LikeButton,
|
||||
userLink,
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
date (value) {
|
||||
// @TODO: Vue doesn't support this so we cant user preference
|
||||
return moment(value).toDate().toString();
|
||||
},
|
||||
mixins: [externalLinks],
|
||||
},
|
||||
mixins: [
|
||||
externalLinks, userStateMixin, LikeLogicMixin,
|
||||
CopyToClipboardMixin,
|
||||
],
|
||||
props: {
|
||||
msg: {},
|
||||
msg: {
|
||||
type: Object,
|
||||
},
|
||||
groupId: {
|
||||
type: String,
|
||||
},
|
||||
privateMessageMode: {
|
||||
type: Boolean,
|
||||
},
|
||||
userSentMessage: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
delete: deleteIcon,
|
||||
report: reportIcon,
|
||||
copy: copyIcon,
|
||||
menuIcon,
|
||||
}),
|
||||
reported: false,
|
||||
};
|
||||
@@ -175,19 +416,100 @@ export default {
|
||||
isMessageReported () {
|
||||
return (this.msg.flags && this.msg.flags[this.user.id]) || this.reported;
|
||||
},
|
||||
messageDateForSystemMessage () {
|
||||
return this.isSystemMessage ? this.messageDate : '';
|
||||
},
|
||||
messageDate () {
|
||||
const date = moment(this.msg.timestamp).toDate();
|
||||
return date.toString();
|
||||
},
|
||||
userIsModerator () {
|
||||
return this.hasPermission(this.user, 'moderator');
|
||||
},
|
||||
isSystemMessage () {
|
||||
return this.msg.uuid === 'system';
|
||||
},
|
||||
canLikeMessage () {
|
||||
if (this.isSystemMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.privateMessageMode) {
|
||||
return Boolean(this.msg.uniqueMessageId);
|
||||
}
|
||||
|
||||
return this.msg.id;
|
||||
},
|
||||
canDeleteMessage () {
|
||||
return this.privateMessageMode
|
||||
|| this.msg.uuid === this.user._id
|
||||
|| this.userIsModerator;
|
||||
},
|
||||
canReportMessage () {
|
||||
if (this.privateMessageMode) {
|
||||
return !this.isMessageReported;
|
||||
}
|
||||
return (this.user.flags.communityGuidelinesAccepted && this.msg.uuid !== 'system')
|
||||
&& (!this.isMessageReported || this.userIsModerator);
|
||||
},
|
||||
isUserMentioned () {
|
||||
const message = this.msg;
|
||||
|
||||
if (message.highlight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = this;
|
||||
const displayName = user.profile.name;
|
||||
const { username } = user.auth.local;
|
||||
const pattern = `@(${escapeRegExp(displayName)}|${escapeRegExp(username)})(\\b)`;
|
||||
message.highlight = new RegExp(pattern, 'i').test(message.text);
|
||||
|
||||
return message.highlight;
|
||||
},
|
||||
flagCountDescription () {
|
||||
if (!this.msg.flagCount) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this.msg.flagCount < CHAT_FLAG_LIMIT_FOR_HIDING) {
|
||||
return 'Message flagged once, not hidden';
|
||||
}
|
||||
|
||||
if (this.msg.flagCount < CHAT_FLAG_FROM_SHADOW_MUTE) {
|
||||
return 'Message hidden';
|
||||
}
|
||||
|
||||
return 'Message hidden (shadow-muted)';
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$emit('message-card-mounted');
|
||||
this.handleExternalLinks();
|
||||
this.mapProfileLinksToModal();
|
||||
},
|
||||
updated () {
|
||||
this.handleExternalLinks();
|
||||
this.mapProfileLinksToModal();
|
||||
},
|
||||
methods: {
|
||||
mapProfileLinksToModal () {
|
||||
const links = this.$refs.markdownContainer.getElementsByTagName('a');
|
||||
for (let i = 0; i < links.length; i += 1) {
|
||||
let link = links[i].pathname;
|
||||
|
||||
// Internet Explorer does not provide the leading slash character in the pathname
|
||||
link = link.charAt(0) === '/' ? link : `/${link}`;
|
||||
|
||||
if (link.startsWith('/profile/')) {
|
||||
links[i].onclick = ev => {
|
||||
ev.preventDefault();
|
||||
this.$router.push({ path: link });
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
report () {
|
||||
this.$root.$on('habitica:report-result', data => {
|
||||
if (data.ok) {
|
||||
@@ -199,16 +521,29 @@ export default {
|
||||
|
||||
this.$root.$emit('habitica::report-chat', {
|
||||
message: this.msg,
|
||||
groupId: 'privateMessage',
|
||||
groupId: this.groupId,
|
||||
});
|
||||
},
|
||||
async remove () {
|
||||
if (!window.confirm(this.$t('areYouSureDeleteMessage'))) return; // eslint-disable-line no-alert
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!window.confirm(this.$t('areYouSureDeleteMessage'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = this.msg;
|
||||
this.$emit('message-removed', message);
|
||||
|
||||
if (this.privateMessageMode) {
|
||||
await axios.delete(`/api/v4/inbox/messages/${message.id}`);
|
||||
} else {
|
||||
await this.$store.dispatch('chat:deleteChat', {
|
||||
groupId: this.groupId,
|
||||
chatId: message.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
copy (message) {
|
||||
this.mixinCopyToClipboard(message.text, this.$t('messageCopiedToClipboard'));
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
return renderWithMentions(text, this.user);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
class="container-fluid"
|
||||
class="message-list"
|
||||
>
|
||||
<div class="row loadmore">
|
||||
<div class="loadmore">
|
||||
<div v-if="canLoadMore && !isLoading">
|
||||
<div class="loadmore-divider-holder">
|
||||
<div class="loadmore-divider"></div>
|
||||
@@ -28,7 +28,7 @@
|
||||
<div
|
||||
v-for="(msg) in messages"
|
||||
:key="msg.id"
|
||||
class="row message-row"
|
||||
class="message-row"
|
||||
:class="{ 'margin-right': user._id !== msg.uuid}"
|
||||
>
|
||||
<div
|
||||
@@ -39,28 +39,33 @@
|
||||
class="avatar-left"
|
||||
:member="conversationOpponentUser"
|
||||
:avatar-only="true"
|
||||
:override-top-padding="'14px'"
|
||||
:show-weapon="true"
|
||||
:debug-mode="false"
|
||||
:height="null"
|
||||
:override-top-padding="'0'"
|
||||
:hide-class-badge="true"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
<div
|
||||
class="card"
|
||||
:class="{'card-right': user._id !== msg.uuid, 'card-left': user._id === msg.uuid}"
|
||||
>
|
||||
<message-card
|
||||
:msg="msg"
|
||||
:user-sent-message="user._id === msg.uuid"
|
||||
:group-id="'privateMessage'"
|
||||
:private-message-mode="true"
|
||||
@message-liked="messageLiked"
|
||||
@message-removed="messageRemoved"
|
||||
@show-member-modal="showMemberModal"
|
||||
@message-card-mounted="itemWasMounted"
|
||||
/>
|
||||
</div>
|
||||
<avatar
|
||||
v-if="user && user._id === msg.uuid"
|
||||
class="avatar-right"
|
||||
:member="user"
|
||||
:height="null"
|
||||
:avatar-only="true"
|
||||
:show-weapon="true"
|
||||
:debug-mode="false"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'14px'"
|
||||
:override-top-padding="'0'"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
</div>
|
||||
@@ -71,18 +76,18 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.avatar {
|
||||
width: 170px;
|
||||
min-width: 8rem;
|
||||
height: 120px;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.avatar-right {
|
||||
margin-left: -1rem;
|
||||
.avatar-left, .avatar-right {
|
||||
align-self: center;
|
||||
|
||||
::v-deep .character-sprites {
|
||||
margin-right: 1rem !important;
|
||||
margin-bottom: -5px !important;
|
||||
padding-bottom: 0 !important;
|
||||
margin-top: -1px !important;
|
||||
}
|
||||
|
||||
::v-deep .avatar {
|
||||
margin-left: -1.75rem;
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +96,19 @@
|
||||
margin-bottom: 1rem;
|
||||
padding: 0rem;
|
||||
width: 684px;
|
||||
|
||||
}
|
||||
|
||||
.message-list {
|
||||
width: 100%;
|
||||
padding-right: 10px;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1.2rem;
|
||||
|
||||
&:not(.margin-right) {
|
||||
.d-flex {
|
||||
@@ -102,26 +116,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1400px) {
|
||||
.message-row {
|
||||
margin-left: -15px;
|
||||
margin-right: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-left {
|
||||
border: 1px solid $purple-500;
|
||||
}
|
||||
|
||||
.card-right {
|
||||
border: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
@@ -280,6 +274,9 @@ export default {
|
||||
// container.style.overflowY = 'scroll';
|
||||
}
|
||||
}, 50),
|
||||
messageLiked (message) {
|
||||
this.$emit('message-liked', message);
|
||||
},
|
||||
messageRemoved (message) {
|
||||
this.$emit('message-removed', message);
|
||||
},
|
||||
|
||||
@@ -692,7 +692,7 @@
|
||||
<div class="form-inline clearfix">
|
||||
<Sprite
|
||||
class="pull-left"
|
||||
:class="'inventory_quest_scroll_' + item.key"
|
||||
:image-name="'inventory_quest_scroll_' + item.key"
|
||||
style="margin-right: 10px"
|
||||
/>
|
||||
<p>{{ item.text() }}</p>
|
||||
|
||||
@@ -107,7 +107,7 @@ export default {
|
||||
if (lastPublishedPost) this.posts.push(lastPublishedPost);
|
||||
|
||||
// If the user is authorized, show any draft
|
||||
if (this.user && this.user.contributor.newsPoster) {
|
||||
if (this.user && (this.user.permissions.news || this.user.permissions.fullAccess)) {
|
||||
this.posts.unshift(
|
||||
...postsFromServer
|
||||
.filter(p => !p.published || moment().isBefore(p.publishDate)),
|
||||
|
||||
@@ -843,7 +843,6 @@ export default {
|
||||
purchasedPlanIdInfo () {
|
||||
if (!this.subscriptionBlocks[this.user.purchased.plan.planId]) {
|
||||
// @TODO: find which subs are in the common
|
||||
// console.log(this.subscriptionBlocks
|
||||
// [this.user.purchased.plan.planId]); // eslint-disable-line
|
||||
return {
|
||||
price: 0,
|
||||
|
||||
@@ -27,27 +27,15 @@
|
||||
@changedPosition="tabSelected($event)"
|
||||
>
|
||||
<div slot="right-item">
|
||||
<div
|
||||
<a
|
||||
v-once
|
||||
id="petLikeToEatMarket"
|
||||
class="drawer-help-text"
|
||||
href="/static/faq#pet-foods"
|
||||
target="_blank"
|
||||
>
|
||||
<span>{{ $t('petLikeToEat') + ' ' }}</span>
|
||||
<span
|
||||
class="svg-icon inline icon-16"
|
||||
v-html="icons.information"
|
||||
></span>
|
||||
</div>
|
||||
<b-popover
|
||||
target="petLikeToEatMarket"
|
||||
:placement="'top'"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="popover-content-text"
|
||||
v-html="$t('petLikeToEatText')"
|
||||
></div>
|
||||
</b-popover>
|
||||
<span>{{ $t('petLikeToEat') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</drawer-header-tabs>
|
||||
</div>
|
||||
@@ -80,7 +68,6 @@
|
||||
import _filter from 'lodash/filter';
|
||||
import { mapState } from '@/libs/store';
|
||||
import inventoryUtils from '@/mixins/inventoryUtils';
|
||||
import svgInformation from '@/assets/svg/information.svg';
|
||||
|
||||
import Drawer from '@/components/ui/drawer';
|
||||
import DrawerSlider from '@/components/ui/drawerSlider';
|
||||
@@ -127,10 +114,6 @@ export default {
|
||||
},
|
||||
],
|
||||
selectedDrawerTab: this.defaultSelectedTab,
|
||||
|
||||
icons: Object.freeze({
|
||||
information: svgInformation,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<div
|
||||
v-for="currency of currencies"
|
||||
:key="currency.key"
|
||||
:needed-currency-only="neededCurrencyOnly"
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
<div
|
||||
@@ -54,6 +55,9 @@ export default {
|
||||
amountNeeded: {
|
||||
type: Number,
|
||||
},
|
||||
neededCurrencyOnly: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -66,34 +70,34 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
currencies () {
|
||||
const currencies = [];
|
||||
currencies.push({
|
||||
const currencies = [{
|
||||
type: 'hourglasses',
|
||||
icon: this.icons.hourglasses,
|
||||
value: this.userHourglasses,
|
||||
});
|
||||
},
|
||||
|
||||
currencies.push({
|
||||
{
|
||||
type: 'gems',
|
||||
icon: this.icons.gem,
|
||||
value: this.userGems,
|
||||
});
|
||||
},
|
||||
|
||||
currencies.push({
|
||||
{
|
||||
type: 'gold',
|
||||
icon: this.icons.gold,
|
||||
value: this.userGold,
|
||||
});
|
||||
}];
|
||||
|
||||
for (const currency of currencies) {
|
||||
if (
|
||||
currency.type === this.currencyNeeded
|
||||
if (currency.type === this.currencyNeeded
|
||||
&& !this.enoughCurrency(this.currencyNeeded, this.amountNeeded)
|
||||
) {
|
||||
currency.notEnough = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.neededCurrencyOnly) {
|
||||
return currencies.filter(curr => curr.type === this.currencyNeeded);
|
||||
}
|
||||
return currencies;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ export const QuestHelperMixin = {
|
||||
case 'quests':
|
||||
return `inventory_quest_scroll_${drop.key}`;
|
||||
case 'mounts':
|
||||
return `Mount_Icon_${drop.key}`;
|
||||
return `stable_Mount_Icon_${drop.key}`;
|
||||
case 'pets':
|
||||
return `stable_Pet-${drop.key}`;
|
||||
default:
|
||||
|
||||
@@ -31,13 +31,6 @@
|
||||
:
|
||||
<a href="mailto:admin@habitica.com">admin@habitica.com</a>
|
||||
<br>
|
||||
{{ $t('generalQuestionsSite') }}
|
||||
:
|
||||
<a
|
||||
target="_blank"
|
||||
@click.prevent="openBugReportModal(true)"
|
||||
> {{ $t('askQuestion') }}</a>
|
||||
<br>
|
||||
{{ $t('businessInquiries') }}
|
||||
:
|
||||
<a href="mailto:admin@habitica.com">admin@habitica.com</a>
|
||||
@@ -54,10 +47,8 @@
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import { goToModForm } from '@/libs/modform';
|
||||
import reportBug from '@/mixins/reportBug.js';
|
||||
|
||||
export default {
|
||||
mixins: [reportBug],
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
|
||||
@@ -66,16 +66,13 @@
|
||||
class="nav-link"
|
||||
>{{ $t('presskit') }}</a>
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-item"
|
||||
tag="li"
|
||||
to="/static/contact"
|
||||
>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
v-once
|
||||
class="nav-link"
|
||||
href="mailto:admin@habitica.com"
|
||||
>{{ $t('contactUs') }}</a>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
v-else
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.twitter svg {
|
||||
.bluesky svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://habitica.fandom.com/wiki/Markdown_Cheat_Sheet"
|
||||
href="https://github.com/HabitRPG/habitica/wiki/Markdown-in-Habitica"
|
||||
:class="cssClass('headings')"
|
||||
>{{ $t('markdownHelpLink') }}</a>
|
||||
</small>
|
||||
|
||||
@@ -129,6 +129,12 @@
|
||||
padding-top: 6px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
|
||||
a {
|
||||
line-height: 1.33;
|
||||
color: $gray-500;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-tab {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
}"
|
||||
>
|
||||
<input
|
||||
ref="textInput"
|
||||
:value="value"
|
||||
class="form-control"
|
||||
:type="inputType"
|
||||
@@ -29,12 +30,15 @@
|
||||
}"
|
||||
:readonly="readonly"
|
||||
:aria-readonly="readonly"
|
||||
autocomplete="off"
|
||||
|
||||
:placeholder="placeholder"
|
||||
@keyup="handleChange"
|
||||
@keyup.enter="$emit('enter')"
|
||||
@blur="$emit('blur')"
|
||||
>
|
||||
</div>
|
||||
<template v-if="!hideErrorLine">
|
||||
<div
|
||||
v-for="issue in invalidIssues"
|
||||
:key="issue"
|
||||
@@ -42,6 +46,7 @@
|
||||
>
|
||||
{{ issue }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -85,6 +90,10 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
hideErrorLine: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -107,6 +116,9 @@ export default {
|
||||
this.wasChanged = true;
|
||||
this.$emit('update:value', value);
|
||||
},
|
||||
focus () {
|
||||
this.$refs.textInput.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -128,4 +140,12 @@ export default {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* this removes safari "save username" UI, we only search for one, we dont want to save it */
|
||||
input::-webkit-contacts-auto-fill-button,
|
||||
input::-webkit-credentials-auto-fill-button {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -29,20 +29,12 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.user-link { // this is the user name
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
|
||||
// currently used in the member-details-new.vue
|
||||
&.smaller {
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
}
|
||||
display: inline-flex !important;
|
||||
|
||||
&.no-tier {
|
||||
color: $gray-50;
|
||||
@@ -111,7 +103,6 @@ export default {
|
||||
'backer',
|
||||
'contributor',
|
||||
'hideTooltip',
|
||||
'smallerStyle',
|
||||
'showBuffed',
|
||||
'context',
|
||||
],
|
||||
@@ -173,7 +164,7 @@ export default {
|
||||
return this.hideTooltip ? '' : achievementsLib.getContribText(this.contributor, this.isNPC) || '';
|
||||
},
|
||||
levelStyle () {
|
||||
return `${this.userLevelStyleFromLevel(this.level, this.isNPC)} ${this.smallerStyle ? 'smaller' : ''}`;
|
||||
return `${this.userLevelStyleFromLevel(this.level, this.isNPC)}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ export function createAnimal (egg, potion, type, _content, userItems) {
|
||||
imageName: type === 'pet' ? `stable_Pet-${animalKey}` : `stable_Mount_Icon_${animalKey}`,
|
||||
eggKey: egg.key,
|
||||
eggName: getText(egg.text),
|
||||
mountName: getText(egg.mountText),
|
||||
potionKey: potion.key,
|
||||
potionName: getText(potion.text),
|
||||
name: _content[`${type}Info`][animalKey].text(),
|
||||
|
||||
102
website/client/src/mixins/autoCompleteHelper.js
Normal file
102
website/client/src/mixins/autoCompleteHelper.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export const autoCompleteHelperMixin = {
|
||||
data () {
|
||||
return {
|
||||
mixinData: {
|
||||
autoComplete: {
|
||||
caretPosition: 0,
|
||||
coords: {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
autoCompleteMixinHandleTab (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
} else {
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinHandleEscape (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectNextAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectPreviousAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
if (this.$refs.autocomplete.selected !== null) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.makeSelection();
|
||||
} else {
|
||||
// no autocomplete selected, newline instead
|
||||
this.$refs.autocomplete.cancel();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinUpdateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
|
||||
this._updateCarretPosition(eventUpdate);
|
||||
}, 250),
|
||||
|
||||
autoCompleteMixinResetCoordsPosition () {
|
||||
this.mixinData.autoComplete.coords = {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
};
|
||||
},
|
||||
|
||||
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
|
||||
_getCoord (e, text) {
|
||||
const caretPosition = text.selectionEnd;
|
||||
this.mixinData.autoComplete.caretPosition = caretPosition;
|
||||
|
||||
const div = document.createElement('div');
|
||||
const span = document.createElement('span');
|
||||
const copyStyle = getComputedStyle(text);
|
||||
|
||||
[].forEach.call(copyStyle, prop => {
|
||||
div.style[prop] = copyStyle[prop];
|
||||
});
|
||||
|
||||
div.style.position = 'absolute';
|
||||
document.body.appendChild(div);
|
||||
div.textContent = text.value.substr(0, caretPosition);
|
||||
span.textContent = text.value.substr(caretPosition) || '.';
|
||||
div.appendChild(span);
|
||||
this.mixinData.autoComplete.coords = {
|
||||
TOP: span.offsetTop,
|
||||
LEFT: span.offsetLeft,
|
||||
};
|
||||
document.body.removeChild(div);
|
||||
},
|
||||
_updateCarretPosition (eventUpdate) {
|
||||
const text = eventUpdate.target;
|
||||
this._getCoord(eventUpdate, text);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import notifications from './notifications';
|
||||
import { NotificationMixins } from './notifications';
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
export const CopyToClipboardMixin = {
|
||||
mixins: [NotificationMixins],
|
||||
methods: {
|
||||
async mixinCopyToClipboard (valueToCopy, notificationToShow = null) {
|
||||
if (navigator.clipboard) {
|
||||
@@ -21,3 +21,5 @@ export default {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default CopyToClipboardMixin;
|
||||
|
||||
@@ -2,54 +2,55 @@ import includes from 'lodash/includes';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
foolPet (pet) {
|
||||
foolPet (pet, prank) {
|
||||
const SPECIAL_PETS = [
|
||||
'Wolf-Veteran',
|
||||
'Wolf-Cerberus',
|
||||
'Dragon-Hydra',
|
||||
'Turkey-Base',
|
||||
'BearCub-Polar',
|
||||
'MantisShrimp-Base',
|
||||
'JackOLantern-Base',
|
||||
'Mammoth-Base',
|
||||
'Tiger-Veteran',
|
||||
'Phoenix-Base',
|
||||
'Turkey-Gilded',
|
||||
'MagicalBee-Base',
|
||||
'Lion-Veteran',
|
||||
'Gryphon-RoyalPurple',
|
||||
'JackOLantern-Ghost',
|
||||
'Jackalope-RoyalPurple',
|
||||
'Orca-Base',
|
||||
'Bear-Veteran',
|
||||
'Hippogriff-Hopeful',
|
||||
'Fox-Veteran',
|
||||
'JackOLantern-Glow',
|
||||
'Gryphon-Gryphatrice',
|
||||
'Gryphatrice-Jubilant',
|
||||
'JackOLantern-RoyalPurple',
|
||||
'BearCub-Polar',
|
||||
'Cactus-Veteran',
|
||||
'Dragon-Hydra',
|
||||
'Dragon-Veteran',
|
||||
'Fox-Veteran',
|
||||
'Gryphatrice-Jubilant',
|
||||
'Gryphon-Gryphatrice',
|
||||
'Gryphon-RoyalPurple',
|
||||
'Hippogriff-Hopeful',
|
||||
'Jackalope-RoyalPurple',
|
||||
'JackOLantern-Base',
|
||||
'JackOLantern-Ghost',
|
||||
'JackOLantern-Glow',
|
||||
'JackOLantern-RoyalPurple',
|
||||
'Lion-Veteran',
|
||||
'MagicalBee-Base',
|
||||
'Mammoth-Base',
|
||||
'MantisShrimp-Base',
|
||||
'Orca-Base',
|
||||
'Phoenix-Base',
|
||||
'Tiger-Veteran',
|
||||
'Turkey-Base',
|
||||
'Turkey-Gilded',
|
||||
'Wolf-Cerberus',
|
||||
'Wolf-Veteran',
|
||||
];
|
||||
const BASE_PETS = [
|
||||
'Wolf',
|
||||
'TigerCub',
|
||||
'PandaCub',
|
||||
'LionCub',
|
||||
'Fox',
|
||||
'FlyingPig',
|
||||
'BearCub',
|
||||
'Dragon',
|
||||
'Cactus',
|
||||
'Dragon',
|
||||
'FlyingPig',
|
||||
'Fox',
|
||||
'LionCub',
|
||||
'PandaCub',
|
||||
'TigerCub',
|
||||
'Wolf',
|
||||
];
|
||||
if (!pet) return 'Pet-TigerCub-Fungi';
|
||||
if (!pet) return `Pet-TigerCub-${prank}`;
|
||||
if (SPECIAL_PETS.indexOf(pet) !== -1) {
|
||||
return 'Pet-Dragon-Fungi';
|
||||
return `Pet-Dragon-${prank}`;
|
||||
}
|
||||
const species = pet.slice(0, pet.indexOf('-'));
|
||||
if (includes(BASE_PETS, species)) {
|
||||
return `Pet-${species}-Fungi`;
|
||||
return `Pet-${species}-${prank}`;
|
||||
}
|
||||
return 'Pet-BearCub-Fungi';
|
||||
return `Pet-BearCub-${prank}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,12 +15,26 @@
|
||||
>
|
||||
{{ $t('messages') }}
|
||||
</h2>
|
||||
<div class="placeholder svg-icon">
|
||||
<!-- placeholder -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-secondary plus-button"
|
||||
:class="{'new-message-mode':showStartNewConversationInput}"
|
||||
@click="triggerStartNewConversationState()"
|
||||
>
|
||||
<div
|
||||
v-if="selectedConversation && selectedConversation.key"
|
||||
class="svg-icon icon-10 color"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<start-new-conversation-input-header
|
||||
v-if="showStartNewConversationInput"
|
||||
@startNewConversation="startConversationByUsername($event)"
|
||||
@cancelNewConversation="showStartNewConversationInput = false"
|
||||
/>
|
||||
<div
|
||||
v-else-if="selectedConversation && selectedConversation.key"
|
||||
class="d-flex selected-conversion"
|
||||
>
|
||||
<router-link
|
||||
@@ -52,25 +66,11 @@
|
||||
@change="toggleOpt()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="filtersConversations.length > 0"
|
||||
class="conversations"
|
||||
>
|
||||
<conversation-item
|
||||
v-for="conversation in filtersConversations"
|
||||
:key="conversation.key"
|
||||
:active-key="selectedConversation.key"
|
||||
:contributor="conversation.contributor"
|
||||
:backer="conversation.backer"
|
||||
:uuid="conversation.key"
|
||||
:display-name="conversation.name"
|
||||
:username="conversation.username"
|
||||
:last-message-date="conversation.date"
|
||||
:last-message-text="conversation.lastMessageText
|
||||
? removeTags(parseMarkdown(conversation.lastMessageText)) : ''"
|
||||
@click="selectConversation(conversation.key)"
|
||||
<pm-conversations-list
|
||||
:filters-conversations="filtersConversations"
|
||||
:selected-conversation="selectedConversation"
|
||||
@selectConversation="selectConversation($event)"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="canLoadMoreConversations"
|
||||
class="btn btn-secondary"
|
||||
@@ -79,28 +79,35 @@
|
||||
{{ $t('loadMore') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="messages-column d-flex flex-column align-items-center">
|
||||
<div
|
||||
v-if="filtersConversations.length === 0
|
||||
&& (!selectedConversation || !selectedConversation.key)"
|
||||
class="empty-messages m-auto text-center empty-sidebar"
|
||||
v-if="user.inbox.optOut"
|
||||
class="disable-background-in-message-list"
|
||||
>
|
||||
<div class="no-messages-box">
|
||||
<div
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon envelope"
|
||||
v-html="icons.messageIcon"
|
||||
></div>
|
||||
<h2 v-once>
|
||||
{{ $t('emptyMessagesLine1') }}
|
||||
</h2>
|
||||
<p v-if="!user.flags.chatRevoked">
|
||||
{{ $t('emptyMessagesLine2') }}
|
||||
</p>
|
||||
</div>
|
||||
class="caption"
|
||||
> {{ $t('PMDisabledCaptionTitle') }}. </span>
|
||||
<span
|
||||
v-once
|
||||
class="text"
|
||||
> {{ $t('PMDisabledCaptionText') }} </span>
|
||||
</div>
|
||||
|
||||
<pm-empty-state
|
||||
v-if="uiState === UI_STATES.NO_CONVERSATIONS"
|
||||
:chat-revoked="user.flags.chatRevoked"
|
||||
@newMessageClicked="showStartNewConversationInput = true"
|
||||
/>
|
||||
|
||||
<pm-new-message-started
|
||||
v-if="uiState === UI_STATES.START_NEW_CONVERSATION && selectedConversation.userStyles"
|
||||
:member-obj="selectedConversation.userStyles"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="filtersConversations.length !== 0 && !selectedConversation.key"
|
||||
v-if="uiState === UI_STATES.NO_CONVERSATIONS_SELECTED"
|
||||
class="empty-messages full-height m-auto text-center"
|
||||
>
|
||||
<div class="no-messages-box">
|
||||
@@ -113,20 +120,7 @@
|
||||
<p v-html="placeholderTexts.description"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedConversation.key && selectedConversationMessages.length === 0"
|
||||
class="empty-messages full-height mt-auto text-center"
|
||||
>
|
||||
<avatar
|
||||
v-if="selectedConversation.userStyles"
|
||||
:member="selectedConversation.userStyles"
|
||||
:avatar-only="true"
|
||||
sprites-margin="0 0 0 -45px"
|
||||
class="center-avatar"
|
||||
/>
|
||||
<h3>{{ $t('beginningOfConversation', {userName: selectedConversation.name}) }}</h3>
|
||||
<p>{{ $t('beginningOfConversationReminder') }}</p>
|
||||
</div>
|
||||
|
||||
<messageList
|
||||
v-if="selectedConversation && selectedConversationMessages.length > 0"
|
||||
ref="chatscroll"
|
||||
@@ -136,16 +130,18 @@
|
||||
:can-load-more="canLoadMore"
|
||||
:is-loading="messagesLoading"
|
||||
@message-removed="messageRemoved"
|
||||
@message-liked="messageLiked"
|
||||
@triggerLoad="infiniteScrollTrigger"
|
||||
/>
|
||||
|
||||
<pm-disabled-state
|
||||
v-if="disabledTexts?.showBottomInfo"
|
||||
:disabled-texts="disabledTexts"
|
||||
/>
|
||||
<div
|
||||
v-if="disabledTexts"
|
||||
class="pm-disabled-caption text-center"
|
||||
v-if="shouldShowInputPanel"
|
||||
class="full-width"
|
||||
>
|
||||
<h4>{{ disabledTexts.title }}</h4>
|
||||
<p>{{ disabledTexts.description }}</p>
|
||||
</div>
|
||||
<div class="full-width">
|
||||
<div
|
||||
class="new-message-row d-flex align-items-center"
|
||||
>
|
||||
@@ -174,7 +170,7 @@
|
||||
:class="{'disabled':newMessageDisabled || newMessage === ''}"
|
||||
@click="sendPrivateMessage()"
|
||||
>
|
||||
{{ $t('send') }}
|
||||
{{ $t('sendMessage') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,8 +180,8 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/variables.scss';
|
||||
@import '~@/assets/scss/colors';
|
||||
@import '~@/assets/scss/variables';
|
||||
|
||||
$pmHeaderHeight: 56px;
|
||||
|
||||
@@ -216,7 +212,11 @@
|
||||
}
|
||||
|
||||
.toggle-switch-outer {
|
||||
display: flex;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -254,13 +254,44 @@
|
||||
letter-spacing: normal;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.empty-messages {
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
|
||||
h3, p {
|
||||
color: $gray-200;
|
||||
margin: 0rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $gray-200;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.no-messages-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 330px;
|
||||
}
|
||||
|
||||
.envelope {
|
||||
color: $gray-400 !important;
|
||||
|
||||
svg {
|
||||
width: 86px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/tiers.scss';
|
||||
@import '~@/assets/scss/variables.scss';
|
||||
@import '~@/assets/scss/colors';
|
||||
@import '~@/assets/scss/tiers';
|
||||
@import '~@/assets/scss/variables';
|
||||
|
||||
$pmHeaderHeight: 56px;
|
||||
$background: $white;
|
||||
@@ -268,10 +299,15 @@
|
||||
.header-bar {
|
||||
height: 56px;
|
||||
background-color: $white;
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
align-items: center;
|
||||
|
||||
.left-header {
|
||||
padding-left: 1.5rem;
|
||||
max-width: 330px;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mail-icon {
|
||||
width: 32px;
|
||||
height: 24px;
|
||||
@@ -285,6 +321,14 @@
|
||||
.placeholder.svg-icon {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.plus-button {
|
||||
padding: 10px 14px;
|
||||
|
||||
&.new-message-mode {
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-height {
|
||||
@@ -316,42 +360,24 @@
|
||||
border-bottom: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.conversations {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-messages {
|
||||
h3, p {
|
||||
color: $gray-200;
|
||||
margin: 0rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $gray-200;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.no-messages-box {
|
||||
.disable-background-in-message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 330px;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
|
||||
color: $yellow-1;
|
||||
background: $yellow-500;
|
||||
width: 100%;
|
||||
|
||||
.caption {
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.envelope {
|
||||
color: $gray-400 !important;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
::v-deep svg {
|
||||
width: 64px;
|
||||
height: 48px;
|
||||
}
|
||||
.text {
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +472,7 @@
|
||||
padding: 1.5rem;
|
||||
|
||||
.guidelines {
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
@@ -458,10 +485,10 @@
|
||||
}
|
||||
|
||||
button {
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
line-height: 1.714;
|
||||
margin-left: 1.5rem;
|
||||
padding: 2px 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
@@ -473,30 +500,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pm-disabled-caption {
|
||||
padding-top: 1em;
|
||||
z-index: 2;
|
||||
|
||||
h4, p {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.left-header {
|
||||
max-width: calc(330px - 2rem); // minus the left padding
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 330px;
|
||||
background-color: $gray-700;
|
||||
@@ -540,7 +543,7 @@
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
|
||||
box-shadow: 0 3px 12px 0 rgba($black, 0.24);
|
||||
box-shadow: 0 3px 12px 0 rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
|
||||
.center-avatar {
|
||||
@@ -549,36 +552,52 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import Vue, { defineComponent } from 'vue';
|
||||
import moment from 'moment';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import axios from 'axios';
|
||||
import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import { mapState } from '@/libs/store';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
import userLink from '@/components/userLink';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch.vue';
|
||||
import userLink from '@/components/userLink.vue';
|
||||
|
||||
import messageList from '@/components/messages/messageList';
|
||||
import messageList from '@/components/messages/messageList.vue';
|
||||
import messageIcon from '@/assets/svg/message.svg';
|
||||
import mail from '@/assets/svg/mail.svg';
|
||||
import conversationItem from '@/components/messages/conversationItem';
|
||||
import faceAvatar from '@/components/faceAvatar';
|
||||
import Avatar from '@/components/avatar';
|
||||
import faceAvatar from '@/components/faceAvatar.vue';
|
||||
import { EVENTS } from '@/libs/events';
|
||||
import PmConversationsList from './pm-conversations-list.vue';
|
||||
import PmEmptyState from './pm-empty-state.vue';
|
||||
import PmDisabledState from './pm-disabled-state.vue';
|
||||
import PmNewMessageStarted from './pm-new-message-started.vue';
|
||||
import StartNewConversationInputHeader from './start-new-conversation-input-header.vue';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import NotificationMixins from '@/mixins/notifications';
|
||||
|
||||
// extract to a shared path
|
||||
const CONVERSATIONS_PER_PAGE = 10;
|
||||
const PM_PER_PAGE = 10;
|
||||
|
||||
export default {
|
||||
const UI_STATES = Object.freeze({
|
||||
LOADING: 'LOADING',
|
||||
NO_CONVERSATIONS: 'NO_CONVERSATIONS',
|
||||
NO_CONVERSATIONS_SELECTED: 'NO_CONVERSATIONS_SELECTED',
|
||||
START_NEW_CONVERSATION: 'START_NEW_CONVERSATION',
|
||||
CONVERSATION_SELECTED: 'CONVERSATION_SELECTED',
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Avatar,
|
||||
StartNewConversationInputHeader,
|
||||
PmNewMessageStarted,
|
||||
PmDisabledState,
|
||||
PmEmptyState,
|
||||
PmConversationsList,
|
||||
messageList,
|
||||
toggleSwitch,
|
||||
conversationItem,
|
||||
userLink,
|
||||
faceAvatar,
|
||||
},
|
||||
@@ -587,7 +606,7 @@ export default {
|
||||
return moment(new Date(value)).fromNow();
|
||||
},
|
||||
},
|
||||
mixins: [styleHelper],
|
||||
mixins: [styleHelper, NotificationMixins],
|
||||
beforeRouteEnter (to, from, next) {
|
||||
next(vm => {
|
||||
const data = vm.$store.state.privateMessageOptions;
|
||||
@@ -610,17 +629,26 @@ export default {
|
||||
icons: Object.freeze({
|
||||
messageIcon,
|
||||
mail,
|
||||
positive: positiveIcon,
|
||||
}),
|
||||
loaded: false,
|
||||
UI_STATES,
|
||||
showStartNewConversationInput: false,
|
||||
newConversationTargetUser: null,
|
||||
loadingConversations: true,
|
||||
showPopover: false,
|
||||
|
||||
/* Conversation-specific data */
|
||||
/**
|
||||
* @type {PrivateMessages.InitiatedConversation}
|
||||
*/
|
||||
initiatedConversation: null,
|
||||
updateConversationsCounter: 0,
|
||||
selectedConversation: {},
|
||||
conversationPage: 0,
|
||||
canLoadMoreConversations: false,
|
||||
/** @type {PrivateMessages.ConversationSummaryMessageEntry[]} */
|
||||
loadedConversations: [],
|
||||
/** @type {Record<string, PrivateMessages.PrivateMessageEntry[]>} */
|
||||
messagesByConversation: {}, // cache {uuid: []}
|
||||
|
||||
newMessage: '',
|
||||
@@ -653,9 +681,15 @@ export default {
|
||||
}];
|
||||
}
|
||||
// Create conversation objects
|
||||
|
||||
/** @type {PrivateMessages.ConversationEntry[]} */
|
||||
const convos = [];
|
||||
|
||||
for (const key in inboxGroup) {
|
||||
if (Object.prototype.hasOwnProperty.call(inboxGroup, key)) {
|
||||
/**
|
||||
* @type {PrivateMessages.ConversationSummaryMessageEntry}
|
||||
*/
|
||||
const recentMessage = inboxGroup[key][0];
|
||||
|
||||
const convoModel = {
|
||||
@@ -709,9 +743,6 @@ export default {
|
||||
|
||||
return ordered;
|
||||
},
|
||||
currentLength () {
|
||||
return this.newMessage.length;
|
||||
},
|
||||
placeholderTexts () {
|
||||
if (this.user.flags.chatRevoked) {
|
||||
return {
|
||||
@@ -724,24 +755,22 @@ export default {
|
||||
description: this.$t('PMPlaceholderDescription'),
|
||||
};
|
||||
},
|
||||
|
||||
disabledTexts () {
|
||||
if (this.user.flags.chatRevoked) {
|
||||
return {
|
||||
enableInput: false,
|
||||
showBottomInfo: true,
|
||||
title: this.$t('PMPlaceholderTitleRevoked'),
|
||||
description: this.$t('chatPrivilegesRevoked'),
|
||||
};
|
||||
}
|
||||
|
||||
if (this.user.inbox.optOut) {
|
||||
return {
|
||||
title: this.$t('PMDisabledCaptionTitle'),
|
||||
description: this.$t('PMDisabledCaptionText'),
|
||||
};
|
||||
}
|
||||
|
||||
if (this.selectedConversation?.key) {
|
||||
if (this.user.inbox.blocks.includes(this.selectedConversation.key)) {
|
||||
return {
|
||||
enableInput: false,
|
||||
showBottomInfo: true,
|
||||
title: this.$t('PMDisabledCaptionTitle'),
|
||||
description: this.$t('PMUnblockUserToSendMessages'),
|
||||
};
|
||||
@@ -749,12 +778,23 @@ export default {
|
||||
|
||||
if (!this.selectedConversation.canReceive) {
|
||||
return {
|
||||
enableInput: false,
|
||||
showBottomInfo: true,
|
||||
title: this.$t('PMCanNotReply'),
|
||||
description: this.$t('PMUserDoesNotReceiveMessages'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.user.inbox.optOut) {
|
||||
return {
|
||||
enableInput: true,
|
||||
showBottomInfo: false,
|
||||
title: this.$t('PMDisabledCaptionTitle'),
|
||||
description: this.$t('PMDisabledCaptionText'),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
optTextSet () {
|
||||
@@ -776,8 +816,51 @@ export default {
|
||||
return '';
|
||||
},
|
||||
newMessageDisabled () {
|
||||
return !this.selectedConversation || !this.selectedConversation.key
|
||||
|| this.disabledTexts !== null;
|
||||
if (this.disabledTexts) {
|
||||
return !this.disabledTexts.enableInput;
|
||||
}
|
||||
|
||||
return [
|
||||
UI_STATES.NO_CONVERSATIONS_SELECTED,
|
||||
UI_STATES.NO_CONVERSATIONS,
|
||||
UI_STATES.LOADING,
|
||||
].includes(this.uiState);
|
||||
},
|
||||
uiState () {
|
||||
if (this.loadingConversations) {
|
||||
return UI_STATES.LOADING;
|
||||
}
|
||||
|
||||
if (this.loadedConversations.length === 0) {
|
||||
return UI_STATES.NO_CONVERSATIONS;
|
||||
}
|
||||
|
||||
// Hiding the "Select a conversation on the left" state,
|
||||
// and just picking the first conversation once it loads, right away
|
||||
// see reload method
|
||||
/* if (!this.selectedConversation.key) {
|
||||
return UI_STATES.NO_CONVERSATIONS_SELECTED;
|
||||
} */
|
||||
|
||||
if (this.selectedConversationMessages.length === 0) {
|
||||
return UI_STATES.START_NEW_CONVERSATION;
|
||||
}
|
||||
|
||||
return UI_STATES.CONVERSATION_SELECTED;
|
||||
},
|
||||
shouldShowInputPanel () {
|
||||
const currentUiState = this.uiState;
|
||||
|
||||
switch (currentUiState) {
|
||||
case UI_STATES.CONVERSATION_SELECTED:
|
||||
case UI_STATES.START_NEW_CONVERSATION: {
|
||||
return true;
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
@@ -787,15 +870,11 @@ export default {
|
||||
// notification click to refresh
|
||||
this.$root.$on(EVENTS.PM_REFRESH, async () => {
|
||||
await this.reload();
|
||||
|
||||
this.selectFirstConversation();
|
||||
});
|
||||
|
||||
// header sync button
|
||||
this.$root.$on(EVENTS.RESYNC_COMPLETED, async () => {
|
||||
await this.reload();
|
||||
|
||||
this.selectFirstConversation();
|
||||
});
|
||||
|
||||
await this.reload();
|
||||
@@ -828,7 +907,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
async reload () {
|
||||
this.loaded = false;
|
||||
this.loadingConversations = true;
|
||||
this.conversationPage = 0;
|
||||
|
||||
this.loadedConversations = [];
|
||||
@@ -838,11 +917,15 @@ export default {
|
||||
|
||||
await this.$store.dispatch('user:markPrivMessagesRead');
|
||||
|
||||
this.loaded = true;
|
||||
await this.selectFirstConversation();
|
||||
|
||||
this.loadingConversations = false;
|
||||
},
|
||||
async loadConversations () {
|
||||
const query = ['/api/v4/inbox/conversations'];
|
||||
query.push(`?page=${this.conversationPage}`);
|
||||
const query = [
|
||||
'/api/v4/inbox/conversations',
|
||||
`?page=${this.conversationPage}`,
|
||||
];
|
||||
this.conversationPage += 1;
|
||||
|
||||
const conversationRes = await axios.get(query.join(''));
|
||||
@@ -850,6 +933,12 @@ export default {
|
||||
this.canLoadMoreConversations = loadedConversations.length === CONVERSATIONS_PER_PAGE;
|
||||
this.loadedConversations.push(...loadedConversations);
|
||||
},
|
||||
messageLiked (message) {
|
||||
const messages = this.messagesByConversation[this.selectedConversation.key];
|
||||
|
||||
const chatIndex = findIndex(messages, chatMessage => chatMessage.id === message.id);
|
||||
messages.splice(chatIndex, 1, message);
|
||||
},
|
||||
messageRemoved (message) {
|
||||
const messages = this.messagesByConversation[this.selectedConversation.key];
|
||||
|
||||
@@ -916,38 +1005,82 @@ export default {
|
||||
this.selectedConversation.lastMessageText = this.newMessage;
|
||||
this.selectedConversation.date = new Date();
|
||||
|
||||
this.scrollToBottom();
|
||||
|
||||
this.$store.dispatch('members:sendPrivateMessage', {
|
||||
toUserId: this.selectedConversation.key,
|
||||
message: this.newMessage,
|
||||
}).then(response => {
|
||||
const newMessage = response.data.data.message;
|
||||
const messageToReset = messages[messages.length - 1];
|
||||
messageToReset.id = newMessage.id; // just set the id, all other infos already set
|
||||
messageToReset.text = newMessage.text; // handle mentions
|
||||
|
||||
// just set the id, all other infos already set
|
||||
messageToReset.id = newMessage.id;
|
||||
messageToReset.text = newMessage.text;
|
||||
messageToReset.uniqueMessageId = newMessage.uniqueMessageId;
|
||||
|
||||
Object.assign(messages[messages.length - 1], messageToReset);
|
||||
this.updateConversationsCounter += 1;
|
||||
});
|
||||
|
||||
this.newMessage = '';
|
||||
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 150);
|
||||
},
|
||||
scrollToBottom () {
|
||||
/**
|
||||
* This method does a couple of things:
|
||||
* - first round:
|
||||
* - tries to scroll down
|
||||
* - in the next tick it triggers it again
|
||||
* (during testing it seemed that the first trigger still had some space left to scroll)
|
||||
* - 2nd round:
|
||||
* - tries to scroll down
|
||||
* - in the next tick it checks if the scrollTop is to most it can scroll down,
|
||||
* if it is, it stops from doing that again
|
||||
* if not, it goes into the next round
|
||||
* - if we reach round 6 it stops completely,
|
||||
* no need to have a endless loop of just scrolling down
|
||||
*/
|
||||
scrollToBottom (callCount = 0) {
|
||||
if (callCount > 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.$refs.chatscroll) {
|
||||
// if the message list component not loaded yet, but scrollToBottom was called
|
||||
// just try again at a later time
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom(callCount + 1);
|
||||
}, 125);
|
||||
return;
|
||||
}
|
||||
|
||||
const chatscrollEl = this.$refs.chatscroll.$el;
|
||||
// chatscrollBeforeTick.scrollTop = chatscrollBeforeTick.scrollHeight;
|
||||
chatscrollEl.scrollTo(0, chatscrollEl.scrollHeight);
|
||||
|
||||
Vue.nextTick(() => {
|
||||
if (!this.$refs.chatscroll) return;
|
||||
const chatscroll = this.$refs.chatscroll.$el;
|
||||
chatscroll.scrollTop = chatscroll.scrollHeight;
|
||||
if (!this.$refs.chatscroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldRetrigger = true;
|
||||
|
||||
if (callCount > 1) {
|
||||
const maxPossibleScrollPos = chatscrollEl.scrollHeight - chatscrollEl.clientHeight;
|
||||
|
||||
if (chatscrollEl.scrollTop === maxPossibleScrollPos) {
|
||||
shouldRetrigger = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRetrigger) {
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom(callCount + 1);
|
||||
}, 125);
|
||||
}
|
||||
});
|
||||
},
|
||||
removeTags (html) {
|
||||
const tmp = document.createElement('DIV');
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || '';
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
if (!text) return null;
|
||||
return habiticaMarkdown.render(String(text));
|
||||
},
|
||||
infiniteScrollTrigger () {
|
||||
// show loading and wait until the loadMore debounced
|
||||
// or else it would trigger on every scrolling-pixel (while not loading)
|
||||
@@ -983,11 +1116,81 @@ export default {
|
||||
this.selectedConversation.canLoadMore = loadedMessages.length === PM_PER_PAGE;
|
||||
this.messagesLoading = false;
|
||||
},
|
||||
selectFirstConversation () {
|
||||
async selectFirstConversation () {
|
||||
if (this.loadedConversations.length > 0) {
|
||||
this.selectConversation(this.loadedConversations[0].uuid, true);
|
||||
await this.selectConversation(this.loadedConversations[0].uuid, true);
|
||||
}
|
||||
},
|
||||
triggerStartNewConversationState () {
|
||||
this.showStartNewConversationInput = true;
|
||||
},
|
||||
async startConversationByUsername (targetUserName) {
|
||||
// check if the target user exists in current conversations, select that conversation
|
||||
/** @type {PrivateMessages.ConversationSummaryMessageEntry} */
|
||||
const foundConversation = this.loadedConversations.find(c => c.username === targetUserName);
|
||||
|
||||
if (foundConversation) {
|
||||
this.selectConversation(foundConversation.uuid);
|
||||
this.showStartNewConversationInput = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let loadedMember = null;
|
||||
|
||||
try {
|
||||
loadedMember = await this.$store.dispatch('members:fetchMemberByUsername', {
|
||||
username: targetUserName,
|
||||
});
|
||||
} catch {
|
||||
loadedMember = null;
|
||||
}
|
||||
|
||||
if (!loadedMember) {
|
||||
this.error(this.$t('targetUserNotExist', { userName: targetUserName }));
|
||||
return;
|
||||
}
|
||||
|
||||
const loadedMemberUUID = loadedMember.id;
|
||||
|
||||
this.showStartNewConversationInput = false;
|
||||
|
||||
// otherwise create a dummy conversation, load messages for that user
|
||||
/**
|
||||
* @type {PrivateMessages.ConversationSummaryMessageEntry}
|
||||
*/
|
||||
const newConversationItem = {
|
||||
uuid: loadedMemberUUID,
|
||||
user: loadedMember.profile.name,
|
||||
username: loadedMember.auth.local.username,
|
||||
contributor: loadedMember.contributor,
|
||||
userStyles: loadedMember,
|
||||
canReceive: loadedMember.inbox.canReceive,
|
||||
timestamp: new Date(),
|
||||
count: 0,
|
||||
text: '',
|
||||
};
|
||||
|
||||
this.loadedConversations.splice(0, 0, newConversationItem);
|
||||
|
||||
this.selectConversation(loadedMemberUUID);
|
||||
|
||||
if (this.messagesByConversation[loadedMemberUUID]) {
|
||||
const messageLengthByConversation = this.messagesByConversation[loadedMemberUUID].length;
|
||||
|
||||
// if messages already exists, update the sidebar entry last message
|
||||
if (messageLengthByConversation > 0) {
|
||||
/** @type {PrivateMessages.PrivateMessageEntry} */
|
||||
const lastMessage = this.messagesByConversation[loadedMemberUUID][messageLengthByConversation - 1];
|
||||
|
||||
newConversationItem.lastMessageText = lastMessage.text;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.newConversationTargetUser = loadedMember;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import userLabel from '../userLabel';
|
||||
import userLabel from '../../components/userLabel.vue';
|
||||
|
||||
import dots from '@/assets/svg/dots.svg';
|
||||
import block from '@/assets/svg/block.svg';
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/colors';
|
||||
|
||||
.action-padding {
|
||||
height: 24px !important;
|
||||
@@ -153,7 +153,7 @@ export default {
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/colors';
|
||||
|
||||
.conversation {
|
||||
padding: 1rem 1.5rem;
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="filtersConversations.length > 0"
|
||||
class="conversations"
|
||||
>
|
||||
<conversation-item
|
||||
v-for="conversation in filtersConversations"
|
||||
:key="conversation.key"
|
||||
:active-key="selectedConversation?.key"
|
||||
:contributor="conversation.contributor"
|
||||
:backer="conversation.backer"
|
||||
:uuid="conversation.key"
|
||||
:display-name="conversation.name"
|
||||
:username="conversation.username"
|
||||
:last-message-date="conversation.date"
|
||||
:last-message-text="conversation.lastMessageText
|
||||
? removeTags(parseMarkdown(conversation.lastMessageText)) : ''"
|
||||
@click="selectConversation(conversation.key)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversations {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import conversationItem from '@/pages/private-messages/pm-conversation-item.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { conversationItem },
|
||||
props: {
|
||||
filtersConversations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedConversation: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeTags (html) {
|
||||
const tmp = document.createElement('DIV');
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || '';
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
if (!text) return null;
|
||||
return habiticaMarkdown.render(String(text));
|
||||
},
|
||||
selectConversation (conversationKey) {
|
||||
this.$emit('selectConversation', conversationKey);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div
|
||||
class="pm-disabled-caption text-center"
|
||||
>
|
||||
<h4>{{ disabledTexts.title }}</h4>
|
||||
<p>{{ disabledTexts.description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.pm-disabled-caption {
|
||||
padding-top: 1.5em;
|
||||
z-index: 2;
|
||||
|
||||
h4, p {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['disabledTexts'],
|
||||
};
|
||||
</script>
|
||||
71
website/client/src/pages/private-messages/pm-empty-state.vue
Normal file
71
website/client/src/pages/private-messages/pm-empty-state.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div
|
||||
class="empty-messages m-auto text-center empty-sidebar"
|
||||
>
|
||||
<div class="no-messages-box">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon envelope mb-4"
|
||||
v-html="icons.mailIcon"
|
||||
></div>
|
||||
<strong
|
||||
v-once
|
||||
class="mb-1"
|
||||
>
|
||||
{{ $t('emptyMessagesLine1') }}
|
||||
</strong>
|
||||
<p v-if="!chatRevoked">
|
||||
{{ $t('emptyMessagesLine2') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary mt-4 d-flex align-items-center"
|
||||
@click="$emit('newMessageClicked')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon icon-10 color mr-2"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
{{ $t('newMessage') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
strong {
|
||||
line-height: 1.71;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.svg-icon.icon-10 {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import mailIcon from '@/assets/svg/mail.svg';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
chatRevoked: Boolean,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
mailIcon,
|
||||
positive: positiveIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user