Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79f83d87d0 | |||
| c9bf157c23 | |||
| e54425f947 | |||
| d953ea14da | |||
| 0c179fee2f | |||
| 05e229ccb0 | |||
| b29d937806 | |||
| 670b6a1563 | |||
| 08f1e2b273 | |||
| ca80f4ee33 | |||
| 65cbee9e75 | |||
| 0f8563c14e | |||
| c4117f99ed | |||
| 27aca19c8c | |||
| 80c18ffadd | |||
| bf2c4eb501 | |||
| ab1828c914 | |||
| b32f79f682 | |||
| 7b69289069 | |||
| a1fb80868f | |||
| 9d4fb80d15 | |||
| 43392f4952 | |||
| 6e46794822 | |||
| 032c95d5c8 | |||
| b40411e219 | |||
| 86410f6bb7 |
@@ -10,6 +10,7 @@ dist/
|
||||
dist-client/
|
||||
apidoc_build/
|
||||
content_cache/
|
||||
i18n_cache/
|
||||
node_modules/
|
||||
|
||||
# Old migrations, disabled
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
# Requesting a feature
|
||||
|
||||
Habitica uses [Trello](https://trello.com/b/EpoYEYod/habitica) to track feature requests. [Read more](https://trello.com/c/odmhIqyW/440-read-first-table-of-contents).
|
||||
Habitica uses [this Google form](https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link) to track feature requests. Please post there rather than creating an issue.
|
||||
|
||||
# Contributing Code
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "4.142.3",
|
||||
"version": "4.143.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -9678,9 +9678,9 @@
|
||||
"integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.25.3",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz",
|
||||
"integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg=="
|
||||
"version": "2.26.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz",
|
||||
"integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw=="
|
||||
},
|
||||
"moment-recur": {
|
||||
"version": "1.0.7",
|
||||
@@ -9694,7 +9694,6 @@
|
||||
"version": "3.5.7",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.7.tgz",
|
||||
"integrity": "sha512-lMtleRT+vIgY/JhhTn1nyGwnSMmJkJELp+4ZbrjctrnBxuLbj6rmLuJFz8W2xUzUqWmqoyVxJLYuC58ZKpcTYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bl": "^2.2.0",
|
||||
"bson": "^1.1.4",
|
||||
@@ -9708,7 +9707,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz",
|
||||
"integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readable-stream": "^2.3.5",
|
||||
"safe-buffer": "^5.1.1"
|
||||
@@ -9717,9 +9715,9 @@
|
||||
}
|
||||
},
|
||||
"mongoose": {
|
||||
"version": "5.9.14",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.14.tgz",
|
||||
"integrity": "sha512-LScxCruQv0YpU/9DasKdThd+3r3PFQbCgesmfa6g0pTDOIiD1A9N9OQsGYrDf+dyUksfLCxJYYF9qpBHLvS1tg==",
|
||||
"version": "5.9.15",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.15.tgz",
|
||||
"integrity": "sha512-dGIDqaQkAJoLl7lsRLy70mDg+VcL1IPOHr/0f23MLF45PtnM5exRdmienfyVjdrSVGgTus+1sMUKef6vSnrDZg==",
|
||||
"requires": {
|
||||
"bson": "^1.1.4",
|
||||
"kareem": "2.3.1",
|
||||
@@ -9732,30 +9730,6 @@
|
||||
"safe-buffer": "5.1.2",
|
||||
"sift": "7.0.1",
|
||||
"sliced": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz",
|
||||
"integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==",
|
||||
"requires": {
|
||||
"readable-stream": "^2.3.5",
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"mongodb": {
|
||||
"version": "3.5.7",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.7.tgz",
|
||||
"integrity": "sha512-lMtleRT+vIgY/JhhTn1nyGwnSMmJkJELp+4ZbrjctrnBxuLbj6rmLuJFz8W2xUzUqWmqoyVxJLYuC58ZKpcTYQ==",
|
||||
"requires": {
|
||||
"bl": "^2.2.0",
|
||||
"bson": "^1.1.4",
|
||||
"denque": "^1.4.1",
|
||||
"require_optional": "^1.0.1",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"saslprep": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mongoose-legacy-pluralize": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.142.3",
|
||||
"version": "4.143.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
@@ -46,9 +46,9 @@
|
||||
"lodash": "^4.17.15",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.25.3",
|
||||
"moment": "^2.26.0",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.9.14",
|
||||
"mongoose": "^5.9.15",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.2",
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"test": {
|
||||
plugins: [
|
||||
["istanbul"],
|
||||
],
|
||||
},
|
||||
},
|
||||
"presets": [
|
||||
["es2015", { modules: false }],
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
],
|
||||
"comments": false,
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// TODO verify if it's needed, added because Axios require Promise in the global scope
|
||||
// and babel-runtime doesn't affect external libraries
|
||||
require('babel-polyfill');
|
||||
|
||||
// Automatically setup SinonJS' sandbox for each test
|
||||
beforeEach(() => {
|
||||
global.sandbox = sinon.createSandbox();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.sandbox.restore();
|
||||
});
|
||||
|
||||
// require all test files
|
||||
const testsContext = require.context('./specs', true, /\.js$/);
|
||||
testsContext.keys().forEach(testsContext);
|
||||
|
||||
// require all .vue and .js files except main.js for coverage.
|
||||
const srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
|
||||
srcContext.keys().forEach(srcContext);
|
||||
@@ -1,40 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// This is a karma config file. For more details see
|
||||
// http://karma-runner.github.io/0.13/config/configuration-file.html
|
||||
// we are also using it with karma-webpack
|
||||
// https://github.com/webpack/karma-webpack
|
||||
|
||||
// Necessary for babel to respect the env version of .babelrc which is necessary
|
||||
// Because inject-loader does not work with ["es2015", { modules: false }] that we use
|
||||
// in order to let webpack2 handle the imports
|
||||
process.env.CHROME_BIN = require('puppeteer').executablePath();
|
||||
// eslint-disable-line no-process-env
|
||||
process.env.BABEL_ENV = process.env.NODE_ENV; // eslint-disable-line no-process-env
|
||||
const webpackConfig = require('../../../webpack/webpack.test.conf');
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
// to run in additional browsers:
|
||||
// 1. install corresponding karma launcher
|
||||
// http://karma-runner.github.io/0.13/config/browsers.html
|
||||
// 2. add it to the `browsers` array below.
|
||||
browsers: ['ChromeHeadless'],
|
||||
frameworks: ['mocha', 'sinon-stub-promise', 'sinon-chai', 'chai-as-promised', 'chai'],
|
||||
reporters: ['spec', 'coverage'],
|
||||
files: ['./index.js'],
|
||||
preprocessors: {
|
||||
'./index.js': ['webpack', 'sourcemap'],
|
||||
},
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
noInfo: true,
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: '../../../coverage/client-unit',
|
||||
reporters: [
|
||||
{ type: 'lcov', subdir: '.' },
|
||||
{ type: 'text-summary' },
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -13,11 +13,11 @@
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addon-actions": "^5.3.18",
|
||||
"@storybook/addon-knobs": "^5.3.18",
|
||||
"@storybook/addon-links": "^5.3.18",
|
||||
"@storybook/addon-notes": "^5.3.18",
|
||||
"@storybook/vue": "^5.3.18",
|
||||
"@storybook/addon-actions": "^5.3.19",
|
||||
"@storybook/addon-knobs": "^5.3.19",
|
||||
"@storybook/addon-links": "^5.3.19",
|
||||
"@storybook/addon-notes": "^5.3.19",
|
||||
"@storybook/vue": "^5.3.19",
|
||||
"@vue/cli-plugin-babel": "^4.3.1",
|
||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||
"@vue/cli-plugin-router": "^4.3.1",
|
||||
@@ -28,8 +28,8 @@
|
||||
"axios": "^0.19.2",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.4.1",
|
||||
"bootstrap-vue": "^2.14.0",
|
||||
"bootstrap": "^4.5.0",
|
||||
"bootstrap-vue": "^2.15.0",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -38,11 +38,11 @@
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"habitica-markdown": "^2.0.0",
|
||||
"hellojs": "^1.18.4",
|
||||
"inspectpack": "^4.4.0",
|
||||
"inspectpack": "^4.5.2",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": "^3.5.1",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.25.3",
|
||||
"moment": "^2.26.0",
|
||||
"nconf": "^0.10.0",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
@@ -56,7 +56,7 @@
|
||||
"vue": "^2.6.11",
|
||||
"vue-cli-plugin-storybook": "^0.6.1",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.1.6",
|
||||
"vue-router": "^3.2.0",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
|
||||
@@ -297,7 +297,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneOffset', 'isUserLoaded']),
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneOffset', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState({ user: 'user.data' }),
|
||||
isStaticPage () {
|
||||
return this.$route.meta.requiresLogin === false;
|
||||
@@ -361,13 +361,55 @@ export default {
|
||||
showSpinner: false,
|
||||
});
|
||||
|
||||
// Set up Error interceptors
|
||||
axios.interceptors.response.use(response => {
|
||||
if (this.user && response.data && response.data.notifications) {
|
||||
this.$set(this.user, 'notifications', response.data.notifications);
|
||||
axios.interceptors.response.use(response => { // Set up Response interceptors
|
||||
// Verify that the user was not updated from another browser/app/client
|
||||
// If it was, sync
|
||||
const { url } = response.config;
|
||||
const { method } = response.config;
|
||||
|
||||
const isApiCall = url.indexOf('api/v4') !== -1;
|
||||
const userV = response.data && response.data.userV;
|
||||
const isCron = url.indexOf('/api/v4/cron') === 0 && method === 'post';
|
||||
|
||||
if (this.isUserLoaded && isApiCall && userV) {
|
||||
const oldUserV = this.user._v;
|
||||
this.user._v = userV;
|
||||
|
||||
// Do not sync again if already syncing
|
||||
const isUserSync = url.indexOf('/api/v4/user') === 0 && method === 'get';
|
||||
const isTasksSync = url.indexOf('/api/v4/tasks/user') === 0 && method === 'get';
|
||||
// exclude chat seen requests because with real time chat they would be too many
|
||||
const isChatSeen = url.indexOf('/chat/seen') !== -1 && method === 'post';
|
||||
// exclude POST /api/v4/cron because the user is synced automatically after cron runs
|
||||
|
||||
// Something has changed on the user object that was not tracked here, sync the user
|
||||
if (userV - oldUserV > 1 && !isCron && !isChatSeen && !isUserSync && !isTasksSync) {
|
||||
Promise.all([
|
||||
this.$store.dispatch('user:fetch', { forceLoad: true }),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true }),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Store the app version from the server
|
||||
const serverAppVersion = response.data && response.data.appVersion;
|
||||
|
||||
if (serverAppVersion && this.$store.state.serverAppVersion !== response.data.appVersion) {
|
||||
this.$store.state.serverAppVersion = serverAppVersion;
|
||||
}
|
||||
|
||||
// Store the notifications, filtering those that have already been read
|
||||
// See store/index.js on why this is necessary
|
||||
if (this.user && response.data && response.data.notifications) {
|
||||
const filteredNotifications = response.data.notifications.filter(serverNotification => {
|
||||
if (this.notificationsRemoved.includes(serverNotification.id)) return false;
|
||||
return true;
|
||||
});
|
||||
this.$set(this.user, 'notifications', filteredNotifications);
|
||||
}
|
||||
|
||||
return response;
|
||||
}, error => {
|
||||
}, error => { // Set up Error interceptors
|
||||
if (error.response.status >= 400) {
|
||||
const isBanned = this.checkForBannedUser(error);
|
||||
if (isBanned === true) return null; // eslint-disable-line consistent-return
|
||||
@@ -425,51 +467,6 @@ export default {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(response => {
|
||||
// Verify that the user was not updated from another browser/app/client
|
||||
// If it was, sync
|
||||
const { url } = response.config;
|
||||
const { method } = response.config;
|
||||
|
||||
const isApiCall = url.indexOf('api/v4') !== -1;
|
||||
const userV = response.data && response.data.userV;
|
||||
const isCron = url.indexOf('/api/v4/cron') === 0 && method === 'post';
|
||||
|
||||
if (this.isUserLoaded && isApiCall && userV) {
|
||||
const oldUserV = this.user._v;
|
||||
this.user._v = userV;
|
||||
|
||||
// Do not sync again if already syncing
|
||||
const isUserSync = url.indexOf('/api/v4/user') === 0 && method === 'get';
|
||||
const isTasksSync = url.indexOf('/api/v4/tasks/user') === 0 && method === 'get';
|
||||
// exclude chat seen requests because with real time chat they would be too many
|
||||
const isChatSeen = url.indexOf('/chat/seen') !== -1 && method === 'post';
|
||||
// exclude POST /api/v4/cron because the user is synced automatically after cron runs
|
||||
|
||||
// Something has changed on the user object that was not tracked here, sync the user
|
||||
if (userV - oldUserV > 1 && !isCron && !isChatSeen && !isUserSync && !isTasksSync) {
|
||||
Promise.all([
|
||||
this.$store.dispatch('user:fetch', { forceLoad: true }),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true }),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the client is updated
|
||||
// const serverAppVersion = response.data.appVersion;
|
||||
// let serverAppVersionState = this.$store.state.serverAppVersion;
|
||||
// if (isApiCall && !serverAppVersionState) {
|
||||
// this.$store.state.serverAppVersion = serverAppVersion;
|
||||
// } else if (isApiCall && serverAppVersionState !== serverAppVersion) {
|
||||
// if (document.activeElement.tagName !== 'INPUT'
|
||||
// || confirm(this.$t('habiticaHasUpdated'))) {
|
||||
// location.reload(true);
|
||||
// }
|
||||
// }
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
// Setup listener for title
|
||||
this.$store.watch(state => state.title, title => {
|
||||
document.title = title;
|
||||
|
||||
@@ -22,9 +22,15 @@
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_202006 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -241px -582px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -391px -296px;
|
||||
background-position: -815px -179px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
@@ -48,7 +54,7 @@
|
||||
}
|
||||
.scene_vikte {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -241px -582px;
|
||||
background-position: -815px 0px;
|
||||
width: 157px;
|
||||
height: 178px;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
width: 221px;
|
||||
height: 39px;
|
||||
}
|
||||
.quest_cow {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px 0px;
|
||||
width: 174px;
|
||||
height: 213px;
|
||||
}
|
||||
.quest_dilatory {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -660px;
|
||||
background-position: -440px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
@@ -18,61 +24,61 @@
|
||||
}
|
||||
.quest_dilatoryDistress2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -1020px;
|
||||
background-position: -1762px -875px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_dilatoryDistress3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -232px;
|
||||
background-position: 0px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dilatory_derby {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -672px;
|
||||
background-position: -1320px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dolphin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -232px;
|
||||
background-position: -220px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dustbunnies {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px 0px;
|
||||
background-position: -440px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_egg {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -359px;
|
||||
background-position: -1762px -214px;
|
||||
width: 165px;
|
||||
height: 207px;
|
||||
}
|
||||
.quest_evilsanta {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -1171px;
|
||||
background-position: -1762px -1026px;
|
||||
width: 118px;
|
||||
height: 131px;
|
||||
}
|
||||
.quest_evilsanta2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -452px;
|
||||
background-position: -660px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_falcon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -452px;
|
||||
background-position: 0px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_ferret {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -452px;
|
||||
background-position: -220px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
@@ -84,13 +90,13 @@
|
||||
}
|
||||
.quest_ghost_stag {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -220px;
|
||||
background-position: -660px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_goldenknight1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -440px;
|
||||
background-position: -880px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
@@ -108,19 +114,19 @@
|
||||
}
|
||||
.quest_gryphon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -874px -1332px;
|
||||
background-position: -1091px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_guineapig {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -672px;
|
||||
background-position: -220px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_harpy {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -672px;
|
||||
background-position: -440px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
@@ -132,109 +138,109 @@
|
||||
}
|
||||
.quest_hippo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -220px;
|
||||
background-position: -880px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_horse {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -440px;
|
||||
background-position: -1100px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_kangaroo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -660px;
|
||||
background-position: -220px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_kraken {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1308px -1332px;
|
||||
background-position: -874px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_lostMasterclasser1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -892px;
|
||||
background-position: -1100px -660px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_lostMasterclasser2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -892px;
|
||||
background-position: 0px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_lostMasterclasser3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -892px;
|
||||
background-position: -220px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_mayhemMistiflying1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -869px;
|
||||
background-position: -1762px -573px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_mayhemMistiflying2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -892px;
|
||||
background-position: -660px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_mayhemMistiflying3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px 0px;
|
||||
background-position: -880px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_monkey {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -220px;
|
||||
background-position: -1100px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moon1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -648px;
|
||||
background-position: -1540px -214px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_moon2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px 0px;
|
||||
background-position: -1320px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moon3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -880px;
|
||||
background-position: -1320px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1112px;
|
||||
background-position: -1320px -660px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -1112px;
|
||||
background-position: -1320px -880px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -1112px;
|
||||
background-position: 0px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_nudibranch {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -214px;
|
||||
background-position: -1540px -1082px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
@@ -246,91 +252,91 @@
|
||||
}
|
||||
.quest_owl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -1112px;
|
||||
background-position: -440px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_peacock {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -1082px;
|
||||
background-position: -1540px -648px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_penguin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -175px;
|
||||
background-position: 0px -1697px;
|
||||
width: 190px;
|
||||
height: 183px;
|
||||
}
|
||||
.quest_pterodactyl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -1112px;
|
||||
background-position: -880px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_rat {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -1112px;
|
||||
background-position: -1100px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_robot {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -1112px;
|
||||
background-position: -440px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_rock {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -431px;
|
||||
background-position: -1540px -865px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_rooster {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px 0px;
|
||||
background-position: -1525px -1332px;
|
||||
width: 213px;
|
||||
height: 174px;
|
||||
}
|
||||
.quest_ruby {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -440px;
|
||||
background-position: -1320px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_sabretooth {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -892px;
|
||||
background-position: -220px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_seaserpent {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -892px;
|
||||
background-position: -1100px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_sheep {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px 0px;
|
||||
background-position: -660px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_silver {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -672px;
|
||||
background-position: 0px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_slime {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -672px;
|
||||
background-position: -880px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_sloth {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px 0px;
|
||||
background-position: -1100px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
@@ -342,7 +348,7 @@
|
||||
}
|
||||
.quest_snake {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1091px -1332px;
|
||||
background-position: -1308px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
@@ -354,37 +360,37 @@
|
||||
}
|
||||
.quest_squirrel {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -452px;
|
||||
background-position: -660px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_stoikalmCalamity1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -567px;
|
||||
background-position: -1762px -422px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_stoikalmCalamity2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -220px;
|
||||
background-position: -880px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_stoikalmCalamity3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -232px;
|
||||
background-position: -660px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_taskwoodsTerror1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -718px;
|
||||
background-position: -1762px -724px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_taskwoodsTerror2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -865px;
|
||||
background-position: -1540px -431px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
@@ -394,9 +400,3 @@
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_treeling {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1525px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 430 KiB After Width: | Height: | Size: 431 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 152 KiB |
@@ -16,7 +16,7 @@
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="heroClass in classes"
|
||||
:key="heroClass"
|
||||
:key="`${heroClass}-avatar`"
|
||||
class="col-md-3"
|
||||
>
|
||||
<div @click="selectedClass = heroClass">
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-for="heroClass in classes"
|
||||
:key="heroClass"
|
||||
:key="`${heroClass}-explanation`"
|
||||
>
|
||||
<div
|
||||
v-if="selectedClass === heroClass"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
class="avatar"
|
||||
:member="user"
|
||||
:avatar-only="true"
|
||||
:hide-class-badge="true"
|
||||
:with-background="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@ export default {
|
||||
this.html = response.data.html;
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('bv::show::modal');
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
<div class="col-12 col-md-6 text-right">
|
||||
<div
|
||||
class="box"
|
||||
class="box member-count"
|
||||
@click="showMemberModal()"
|
||||
>
|
||||
<div
|
||||
@@ -93,6 +93,7 @@
|
||||
:members="members"
|
||||
:challenge-id="challengeId"
|
||||
@member-selected="openMemberProgressModal"
|
||||
@opened="initialMembersLoad()"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 text-right">
|
||||
@@ -279,6 +280,10 @@
|
||||
font-size: 20px;
|
||||
vertical-align: bottom;
|
||||
|
||||
&.member-count:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
@@ -364,6 +369,7 @@ export default {
|
||||
}),
|
||||
challenge: {},
|
||||
members: [],
|
||||
membersLoaded: false,
|
||||
tasksByType: {
|
||||
habit: [],
|
||||
daily: [],
|
||||
@@ -427,8 +433,6 @@ export default {
|
||||
this.$router.push('/challenges/findChallenges');
|
||||
return;
|
||||
}
|
||||
this.members = await this
|
||||
.loadMembers({ challengeId: this.searchId, includeAllPublicFields: true });
|
||||
const tasks = await this.$store.dispatch('tasks:getChallengeTasks', { challengeId: this.searchId });
|
||||
this.tasksByType = {
|
||||
habit: [],
|
||||
@@ -454,7 +458,22 @@ export default {
|
||||
}
|
||||
return this.$store.dispatch('members:getChallengeMembers', payload);
|
||||
},
|
||||
initialMembersLoad () {
|
||||
this.$store.state.memberModalOptions.loading = true;
|
||||
if (!this.membersLoaded) {
|
||||
this.membersLoaded = true;
|
||||
|
||||
this.loadMembers({
|
||||
challengeId: this.searchId,
|
||||
includeAllPublicFields: true,
|
||||
}).then(m => {
|
||||
this.members.push(...m);
|
||||
this.$store.state.memberModalOptions.loading = false;
|
||||
});
|
||||
} else {
|
||||
this.$store.state.memberModalOptions.loading = false;
|
||||
}
|
||||
},
|
||||
editTask (task) {
|
||||
this.taskFormPurpose = 'edit';
|
||||
this.editingTask = cloneDeep(task);
|
||||
@@ -489,6 +508,8 @@ export default {
|
||||
this.tasksByType[task.type].splice(index, 1);
|
||||
},
|
||||
showMemberModal () {
|
||||
this.initialMembersLoad();
|
||||
|
||||
this.$root.$emit('habitica:show-member-modal', {
|
||||
challengeId: this.challenge._id,
|
||||
groupId: 'challenge', // @TODO: change these terrible settings
|
||||
@@ -501,8 +522,8 @@ export default {
|
||||
async joinChallenge () {
|
||||
this.user.challenges.push(this.searchId);
|
||||
this.challenge = await this.$store.dispatch('challenges:joinChallenge', { challengeId: this.searchId });
|
||||
this.members = await this
|
||||
.loadMembers({ challengeId: this.searchId, includeAllPublicFields: true });
|
||||
this.membersLoaded = false;
|
||||
this.members = [];
|
||||
|
||||
await this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true });
|
||||
},
|
||||
@@ -511,10 +532,11 @@ export default {
|
||||
},
|
||||
async updateChallenge () {
|
||||
this.challenge = await this.$store.dispatch('challenges:getChallenge', { challengeId: this.searchId });
|
||||
this.members = await this
|
||||
.loadMembers({ challengeId: this.searchId, includeAllPublicFields: true });
|
||||
this.membersLoaded = false;
|
||||
this.members = [];
|
||||
},
|
||||
closeChallenge () {
|
||||
this.initialMembersLoad();
|
||||
this.$root.$emit('bv::show::modal', 'close-challenge-modal');
|
||||
},
|
||||
edit () {
|
||||
|
||||
@@ -223,7 +223,7 @@ export default {
|
||||
created () {
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'copyAsTodo');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica::copy-as-todo');
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -122,10 +122,10 @@ export default {
|
||||
};
|
||||
},
|
||||
},
|
||||
created () {
|
||||
mounted () {
|
||||
this.$root.$on('habitica::report-chat', this.handleReport);
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica::report-chat', this.handleReport);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
import extend from 'lodash/extend';
|
||||
import groupUtilities from '@/mixins/groupsUtilities';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { mapState, mapGetters } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import startQuestModal from './startQuestModal';
|
||||
import questDetailsModal from './questDetailsModal';
|
||||
@@ -447,6 +447,7 @@ export default {
|
||||
bronzeGuildBadgeIcon,
|
||||
}),
|
||||
members: [],
|
||||
membersLoaded: false,
|
||||
selectedQuest: {},
|
||||
chat: {
|
||||
submitDisable: false,
|
||||
@@ -455,7 +456,12 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
...mapGetters({
|
||||
partyMembers: 'party:members',
|
||||
}),
|
||||
partyStore () {
|
||||
return this.$store.state.party;
|
||||
},
|
||||
@@ -487,10 +493,15 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
async mounted () {
|
||||
if (this.isParty) this.searchId = 'party';
|
||||
if (!this.searchId) this.searchId = this.groupId;
|
||||
this.load();
|
||||
await this.fetchGuild();
|
||||
|
||||
this.$root.$on('updatedGroup', this.onGroupUpdate);
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('updatedGroup', this.onGroupUpdate);
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.$set(this, 'searchId', to.params.groupId);
|
||||
@@ -501,19 +512,9 @@ export default {
|
||||
acceptCommunityGuidelines () {
|
||||
this.$store.dispatch('user:set', { 'flags.communityGuidelinesAccepted': true });
|
||||
},
|
||||
async load () {
|
||||
if (this.isParty) {
|
||||
this.searchId = 'party';
|
||||
// @TODO: Set up from old client. Decide what we need and what we don't
|
||||
// Check Desktop notifs
|
||||
// Load invites
|
||||
}
|
||||
await this.fetchGuild();
|
||||
|
||||
this.$root.$on('updatedGroup', group => {
|
||||
const updatedGroup = extend(this.group, group);
|
||||
this.$set(this.group, updatedGroup);
|
||||
});
|
||||
onGroupUpdate (group) {
|
||||
const updatedGroup = extend(this.group, group);
|
||||
this.$set(this.group, updatedGroup);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -531,6 +532,26 @@ export default {
|
||||
return this.$store.dispatch('members:getGroupMembers', payload);
|
||||
},
|
||||
showMemberModal () {
|
||||
this.$store.state.memberModalOptions.loading = true;
|
||||
|
||||
if (this.isParty) {
|
||||
this.membersLoaded = true;
|
||||
this.members = this.partyMembers;
|
||||
this.$store.state.memberModalOptions.loading = false;
|
||||
} else if (!this.membersLoaded) {
|
||||
this.membersLoaded = true;
|
||||
|
||||
this.loadMembers({
|
||||
groupId: this.group._id,
|
||||
includeAllPublicFields: true,
|
||||
}).then(m => {
|
||||
this.members.push(...m);
|
||||
this.$store.state.memberModalOptions.loading = false;
|
||||
});
|
||||
} else {
|
||||
this.$store.state.memberModalOptions.loading = false;
|
||||
}
|
||||
|
||||
this.$root.$emit('habitica:show-member-modal', {
|
||||
groupId: this.group._id,
|
||||
group: this.group,
|
||||
@@ -565,19 +586,13 @@ export default {
|
||||
|
||||
const groupId = this.searchId === 'party' ? this.user.party._id : this.searchId;
|
||||
if (this.hasUnreadMessages(groupId)) {
|
||||
// Delay by 1sec to make sure it returns after
|
||||
// other requests that don't have the notification marked as read
|
||||
setTimeout(() => {
|
||||
this.$store.dispatch('chat:markChatSeen', { groupId });
|
||||
this.$delete(this.user.newMessages, groupId);
|
||||
}, 1000);
|
||||
const notification = this.user
|
||||
.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId);
|
||||
const notificationId = notification && notification.id;
|
||||
this.$store.dispatch('chat:markChatSeen', { groupId, notificationId });
|
||||
}
|
||||
|
||||
this.members = await this.loadMembers({
|
||||
groupId: this.group._id,
|
||||
includeAllPublicFields: true,
|
||||
});
|
||||
},
|
||||
// returns the notification id or false
|
||||
hasUnreadMessages (groupId) {
|
||||
if (this.user.newMessages[groupId]) return true;
|
||||
|
||||
|
||||
@@ -99,14 +99,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedPage === 'members'">
|
||||
<loading-gryphon v-if="loading" />
|
||||
<div
|
||||
v-if="selectedPage === 'members' && !loading"
|
||||
:class="{'mt-1': invites.length === 0}"
|
||||
>
|
||||
<div
|
||||
v-for="(member, index) in sortedMembers"
|
||||
:key="member._id"
|
||||
class="row"
|
||||
>
|
||||
<div class="col-11 no-padding-left">
|
||||
<member-details :member="member" />
|
||||
<member-details
|
||||
:member="member"
|
||||
:class-badge-position="'next-to-name'"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-1 actions">
|
||||
<b-dropdown right="right">
|
||||
@@ -201,7 +208,7 @@
|
||||
class="row gradient"
|
||||
></div>
|
||||
</div>
|
||||
<div v-if="selectedPage === 'invites'">
|
||||
<div v-if="selectedPage === 'invites' && !loading">
|
||||
<div
|
||||
v-for="(member, index) in invites"
|
||||
:key="member._id"
|
||||
@@ -234,14 +241,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('close') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -254,11 +253,6 @@
|
||||
box-shadow: 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background-color: #edecee;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.small-text, .character-name {
|
||||
color: #878190;
|
||||
}
|
||||
@@ -270,6 +264,8 @@
|
||||
.modal-body {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.member-details {
|
||||
@@ -378,6 +374,7 @@ import isEmpty from 'lodash/isEmpty';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import removeMemberModal from '@/components/members/removeMemberModal';
|
||||
import loadingGryphon from '@/components/ui/loadingGryphon';
|
||||
import MemberDetails from '../memberDetails';
|
||||
import removeIcon from '@/assets/members/remove.svg';
|
||||
import messageIcon from '@/assets/members/message.svg';
|
||||
@@ -388,6 +385,7 @@ export default {
|
||||
components: {
|
||||
MemberDetails,
|
||||
removeMemberModal,
|
||||
loadingGryphon,
|
||||
},
|
||||
props: ['hideBadge'],
|
||||
data () {
|
||||
@@ -474,6 +472,9 @@ export default {
|
||||
challengeId () {
|
||||
return this.$store.state.memberModalOptions.challengeId;
|
||||
},
|
||||
loading () {
|
||||
return this.$store.state.memberModalOptions.loading;
|
||||
},
|
||||
sortedMembers () {
|
||||
let sortedMembers = this.members.slice(); // shallow clone to avoid infinite loop
|
||||
|
||||
@@ -504,16 +505,6 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
groupId () {
|
||||
// @TODO: We might not need this since groupId is computed now
|
||||
this.getMembers();
|
||||
},
|
||||
challengeId () {
|
||||
this.getMembers();
|
||||
},
|
||||
group () {
|
||||
this.getMembers();
|
||||
},
|
||||
// Watches `searchTerm` and if present, performs a `searchMembers` action
|
||||
// and usual `getMembers` otherwise
|
||||
searchTerm () {
|
||||
@@ -537,7 +528,7 @@ export default {
|
||||
this.getMembers();
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:show-member-modal');
|
||||
},
|
||||
methods: {
|
||||
@@ -558,8 +549,9 @@ export default {
|
||||
});
|
||||
},
|
||||
async getMembers () {
|
||||
const { groupId } = this;
|
||||
this.members = this.$store.state.memberModalOptions.viewingMembers;
|
||||
|
||||
const { groupId } = this;
|
||||
if (groupId && groupId !== 'challenge') {
|
||||
const invites = await this.$store.dispatch('members:getGroupInvites', {
|
||||
groupId,
|
||||
@@ -567,8 +559,6 @@ export default {
|
||||
});
|
||||
this.invites = invites;
|
||||
}
|
||||
|
||||
this.members = this.$store.state.memberModalOptions.viewingMembers;
|
||||
},
|
||||
async clickMember (uid, forceShow) {
|
||||
const user = this.$store.state.user.data;
|
||||
|
||||
@@ -209,7 +209,7 @@ export default {
|
||||
|
||||
this.$root.$on('selectQuest', this.selectQuest);
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('selectQuest', this.selectQuest);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -222,8 +222,8 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'invite-modal');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.off('inviteModal::inviteToGroup');
|
||||
beforeDestroy () {
|
||||
this.$root.$off('inviteModal::inviteToGroup');
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
@@ -250,6 +250,7 @@ export default {
|
||||
groupId: party.data._id,
|
||||
viewingMembers: this.partyMembers,
|
||||
group: party.data,
|
||||
fetchMoreMembers: p => this.$store.dispatch('members:getGroupMembers', p),
|
||||
});
|
||||
},
|
||||
setPartyMembersWidth ($event) {
|
||||
|
||||
@@ -182,10 +182,10 @@ export default {
|
||||
remove () {
|
||||
if (this.notification.type === 'NEW_CHAT_MESSAGE') {
|
||||
const groupId = this.notification.data.group.id;
|
||||
this.$store.dispatch('chat:markChatSeen', { groupId });
|
||||
if (this.user.newMessages[groupId]) {
|
||||
this.$delete(this.user.newMessages, groupId);
|
||||
}
|
||||
this.$store.dispatch('chat:markChatSeen', {
|
||||
groupId,
|
||||
notificationId: this.notification.id,
|
||||
});
|
||||
} else {
|
||||
this.readNotification({ notificationId: this.notification.id });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<base-notification
|
||||
v-if="worldBoss.active"
|
||||
v-if="worldBoss && worldBoss.active"
|
||||
:can-remove="false"
|
||||
:notification="{}"
|
||||
:read-after-click="false"
|
||||
@@ -11,10 +11,16 @@
|
||||
class="background"
|
||||
>
|
||||
<div class="text">
|
||||
<div class="title">
|
||||
<div
|
||||
v-once
|
||||
class="title"
|
||||
>
|
||||
{{ $t('worldBoss') }}
|
||||
</div>
|
||||
<div class="sub-title">
|
||||
<div
|
||||
v-once
|
||||
class="sub-title"
|
||||
>
|
||||
{{ $t('questDysheartenerText') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,6 +46,7 @@
|
||||
</div>
|
||||
<div class="pending-damage">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.sword"
|
||||
></div>
|
||||
@@ -182,11 +189,13 @@ export default {
|
||||
sword,
|
||||
}),
|
||||
questData,
|
||||
worldBoss: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
worldBoss: 'worldState.data.worldBoss',
|
||||
}),
|
||||
bossHp () {
|
||||
if (this.worldBoss && this.worldBoss.progress) {
|
||||
return this.worldBoss.progress.hp;
|
||||
@@ -195,8 +204,7 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
const result = await this.$store.dispatch('worldState:getWorldState');
|
||||
this.worldBoss = result.worldBoss;
|
||||
await this.$store.dispatch('worldState:getWorldState');
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
|
||||
@@ -124,7 +124,7 @@ export default {
|
||||
mounted () {
|
||||
this.$root.$on('hatchedPet::open', this.openDialog);
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('hatchedPet::open', this.openDialog);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -102,7 +102,7 @@ export default {
|
||||
mounted () {
|
||||
this.$root.$on('habitica::mount-raised', this.openDialog);
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica::mount-raised', this.openDialog);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class="create-dropdown"
|
||||
:text="text"
|
||||
no-flip="no-flip"
|
||||
@show="$emit('opened')"
|
||||
>
|
||||
<b-dropdown-form
|
||||
:disabled="false"
|
||||
@@ -10,10 +11,14 @@
|
||||
>
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
class="form-control"
|
||||
class="form-control member-input"
|
||||
type="text"
|
||||
>
|
||||
</b-dropdown-form>
|
||||
<loading-gryphon
|
||||
v-if="loading"
|
||||
:height="32"
|
||||
/>
|
||||
<b-dropdown-item
|
||||
v-for="member in memberResults"
|
||||
:key="member._id"
|
||||
@@ -25,13 +30,25 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.create-dropdown ::v-deep form.b-dropdown-form {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.create-dropdown ::v-deep ul.dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// @TODO: how do we subclass this rather than type checking?
|
||||
import challengeMemberSearchMixin from '@/mixins/challengeMemberSearch';
|
||||
import loadingGryphon from '@/components/ui/loadingGryphon';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
loadingGryphon,
|
||||
},
|
||||
mixins: [challengeMemberSearchMixin],
|
||||
props: {
|
||||
text: {
|
||||
@@ -52,6 +69,11 @@ export default {
|
||||
memberResults: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
loading () {
|
||||
return this.$store.state.memberModalOptions.loading;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
memberResults () {
|
||||
if (this.memberResults.length > 10) this.memberResults.length = 10;
|
||||
|
||||
@@ -226,8 +226,6 @@ export default {
|
||||
beforeDestroy () {
|
||||
this.$el.removeEventListener('selectstart', () => this.handleSelectStart());
|
||||
this.$el.removeEventListener('mouseup', () => this.handleSelectChange());
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -817,10 +817,8 @@ export default {
|
||||
break;
|
||||
}
|
||||
case 'CRON':
|
||||
if (notification.data) {
|
||||
if (notification.data.hp) this.hp(notification.data.hp, 'hp');
|
||||
if (notification.data.mp && this.userHasClass) this.mp(notification.data.mp);
|
||||
}
|
||||
// Not needed because it's shown already by the userHp and userMp watchers
|
||||
// Keeping an empty block so that it gets read
|
||||
break;
|
||||
case 'SCORED_TASK':
|
||||
// Search if it is a read notification
|
||||
|
||||
@@ -125,7 +125,7 @@ export default {
|
||||
});
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica::pay-with-amazon');
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -79,7 +79,7 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'subscription-cancel-modal');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:cancel-subscription-confirm');
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -108,7 +108,7 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'subscription-canceled-modal');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:subscription-canceled');
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -237,7 +237,7 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'payments-success-modal');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.paymentData = {};
|
||||
this.$root.$off('habitica:payments-success');
|
||||
},
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
import _filter from 'lodash/filter';
|
||||
import _map from 'lodash/map';
|
||||
import _throttle from 'lodash/throttle';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { mapState, mapGetters } from '@/libs/store';
|
||||
|
||||
import KeysToKennel from './keysToKennel';
|
||||
import EquipmentSection from './equipmentSection';
|
||||
@@ -221,8 +221,6 @@ export default {
|
||||
|
||||
hideLocked: false,
|
||||
hidePinned: false,
|
||||
|
||||
broken: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -232,6 +230,9 @@ export default {
|
||||
userStats: 'user.data.stats',
|
||||
userItems: 'user.data.items',
|
||||
}),
|
||||
...mapGetters({
|
||||
broken: 'worldState.brokenMarket',
|
||||
}),
|
||||
market () {
|
||||
return shops.getMarketShop(this.user);
|
||||
},
|
||||
@@ -303,10 +304,7 @@ export default {
|
||||
}, 250),
|
||||
},
|
||||
async mounted () {
|
||||
const worldState = await this.$store.dispatch('worldState:getWorldState');
|
||||
this.broken = worldState && worldState.worldBoss
|
||||
&& worldState.worldBoss.extra && worldState.worldBoss.extra.worldDmg
|
||||
&& worldState.worldBoss.extra.worldDmg.market;
|
||||
await this.$store.dispatch('worldState:getWorldState');
|
||||
},
|
||||
methods: {
|
||||
sellItem (itemScope) {
|
||||
|
||||
@@ -196,7 +196,7 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'sell-modal');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('sellItem');
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -456,7 +456,7 @@ import _sortBy from 'lodash/sortBy';
|
||||
import _throttle from 'lodash/throttle';
|
||||
import _groupBy from 'lodash/groupBy';
|
||||
import _map from 'lodash/map';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { mapState, mapGetters } from '@/libs/store';
|
||||
|
||||
import ShopItem from '../shopItem';
|
||||
import Item from '@/components/inventory/item';
|
||||
@@ -503,8 +503,6 @@ export default {
|
||||
|
||||
hideLocked: false,
|
||||
hidePinned: false,
|
||||
|
||||
broken: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -514,6 +512,9 @@ export default {
|
||||
userStats: 'user.data.stats',
|
||||
userItems: 'user.data.items',
|
||||
}),
|
||||
...mapGetters({
|
||||
broken: 'worldState.brokenQuests',
|
||||
}),
|
||||
shop () {
|
||||
return shops.getQuestShop(this.user);
|
||||
},
|
||||
@@ -540,9 +541,7 @@ export default {
|
||||
}, 250),
|
||||
},
|
||||
async mounted () {
|
||||
const worldState = await this.$store.dispatch('worldState:getWorldState');
|
||||
this.broken = worldState && worldState.worldBoss && worldState.worldBoss.extra
|
||||
&& worldState.worldBoss.extra.worldDmg && worldState.worldBoss.extra.worldDmg.quests;
|
||||
await this.$store.dispatch('worldState:getWorldState');
|
||||
},
|
||||
methods: {
|
||||
questItems (category, sortBy, searchBy, hideLocked, hidePinned) {
|
||||
|
||||
@@ -373,7 +373,7 @@ import _sortBy from 'lodash/sortBy';
|
||||
import _throttle from 'lodash/throttle';
|
||||
import _groupBy from 'lodash/groupBy';
|
||||
import _reverse from 'lodash/reverse';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { mapState, mapGetters } from '@/libs/store';
|
||||
|
||||
import Checkbox from '@/components/ui/checkbox';
|
||||
import PinBadge from '@/components/ui/pinBadge';
|
||||
@@ -434,8 +434,6 @@ export default {
|
||||
featuredGearBought: false,
|
||||
|
||||
backgroundUpdate: new Date(),
|
||||
|
||||
broken: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -444,6 +442,9 @@ export default {
|
||||
user: 'user.data',
|
||||
userStats: 'user.data.stats',
|
||||
}),
|
||||
...mapGetters({
|
||||
broken: 'worldState.brokenSeasonalShop',
|
||||
}),
|
||||
|
||||
usersOfficalPinnedItems () {
|
||||
return getOfficialPinnedItems(this.user);
|
||||
@@ -516,14 +517,11 @@ export default {
|
||||
}, 250),
|
||||
},
|
||||
async mounted () {
|
||||
const worldState = await this.$store.dispatch('worldState:getWorldState');
|
||||
this.broken = worldState && worldState.worldBoss && worldState.worldBoss.extra
|
||||
&& worldState.worldBoss.extra.worldDmg && worldState.worldBoss.extra.worldDmg.seasonalShop;
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('buyModal::boughtItem', () => {
|
||||
this.backgroundUpdate = new Date();
|
||||
});
|
||||
|
||||
await this.$store.dispatch('worldState:getWorldState');
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('buyModal::boughtItem');
|
||||
|
||||
@@ -395,7 +395,7 @@ export default {
|
||||
this.searchTextThrottled = this.searchText.toLowerCase();
|
||||
}, 250),
|
||||
},
|
||||
created () {
|
||||
mounted () {
|
||||
this.$root.$on('buyModal::boughtItem', () => {
|
||||
this.backgroundUpdate = new Date();
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
brokenChallengeTask: {},
|
||||
};
|
||||
},
|
||||
created () {
|
||||
mounted () {
|
||||
this.$root.$on('handle-broken-task', task => {
|
||||
this.brokenChallengeTask = { ...task };
|
||||
this.$root.$emit('bv::show::modal', 'broken-task-modal');
|
||||
|
||||
@@ -515,7 +515,7 @@ export default {
|
||||
this.loadCompletedTodos();
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('buyModal::boughtItem');
|
||||
if (this.type !== 'todo') return;
|
||||
this.$root.$off(EVENTS.RESYNC_COMPLETED);
|
||||
|
||||
@@ -1323,7 +1323,7 @@ export default {
|
||||
created () {
|
||||
document.addEventListener('keyup', this.handleEsc);
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('keyup', this.handleEsc);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
mounted () {
|
||||
document.documentElement.addEventListener('click', this._clickOutListener);
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('click', this._clickOutListener);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="loading-gryphon-wrapper">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon loading-gryphon"
|
||||
:style="{width: `${width}px`, height: `${height}px`}"
|
||||
v-html="icons.gryphon"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.loading-gryphon-wrapper {
|
||||
width: 100%;
|
||||
padding-top: 72px;
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
|
||||
.loading-gryphon {
|
||||
color: #6133b4;
|
||||
margin: 0 auto;
|
||||
animation: pulsate 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes pulsate {
|
||||
from { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import gryphon from '@/assets/svg/gryphon.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
height: {
|
||||
type: Number,
|
||||
default: 48,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
gryphon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// The original SVG is 70px tall and 64px wide, we calculate the width based on that
|
||||
// in order to keep the right proportions
|
||||
width () {
|
||||
return (this.height / 70) * 64;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -4,6 +4,11 @@
|
||||
class="profile"
|
||||
>
|
||||
<div class="header">
|
||||
<span
|
||||
class="close-icon svg-icon inline icon-10"
|
||||
@click="close()"
|
||||
v-html="icons.close"
|
||||
></span>
|
||||
<div class="profile-actions">
|
||||
<router-link
|
||||
:to="{ path: '/private-messages', query: { uuid: user._id } }"
|
||||
@@ -559,6 +564,7 @@
|
||||
|
||||
.achievement-wrapper {
|
||||
width: 94px;
|
||||
min-width: 94px !important;
|
||||
max-width: 94px;
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
@@ -717,6 +723,7 @@ import lock from '@/assets/svg/lock.svg';
|
||||
import challenge from '@/assets/svg/challenge.svg';
|
||||
import member from '@/assets/svg/member-icon.svg';
|
||||
import staff from '@/assets/svg/tier-staff.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
|
||||
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
|
||||
|
||||
@@ -742,6 +749,7 @@ export default {
|
||||
lock,
|
||||
member,
|
||||
staff,
|
||||
close: svgClose,
|
||||
}),
|
||||
adminToolsLoaded: false,
|
||||
userIdToMessage: '',
|
||||
@@ -987,6 +995,9 @@ export default {
|
||||
const status = this.achievementsCategories[categoryKey].open;
|
||||
this.achievementsCategories[categoryKey].open = !status;
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'profile');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -45,7 +45,7 @@ export default {
|
||||
this.$root.$emit('bv::show::modal', 'profile');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:show-profile');
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -6,12 +6,14 @@ import axios from 'axios';
|
||||
export function asyncResourceFactory () {
|
||||
return {
|
||||
loadingStatus: 'NOT_LOADED', // NOT_LOADED, LOADING, LOADED
|
||||
appVersionOnLoad: null, // record the server app version the last time the resource was loaded
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadAsyncResource ({
|
||||
store, path, url, deserialize, forceLoad = false,
|
||||
store, path, url, deserialize,
|
||||
forceLoad = false, reloadOnAppVersionChange = false,
|
||||
}) {
|
||||
if (!store) throw new Error('"store" is required and must be the application store.');
|
||||
if (!path) throw new Error('The path to the resource in the application state is required.');
|
||||
@@ -22,7 +24,16 @@ export function loadAsyncResource ({
|
||||
if (!resource) throw new Error(`No resouce found at path "${path}".`);
|
||||
const { loadingStatus } = resource;
|
||||
|
||||
if (loadingStatus === 'LOADED' && !forceLoad) {
|
||||
// Has the server been updated since we last loaded this resource?
|
||||
const appVersionHasChanged = loadingStatus === 'LOADED'
|
||||
&& store.state.serverAppVersion
|
||||
&& store.state.serverAppVersion !== resource.appVersionOnLoad;
|
||||
|
||||
let shouldUpdate = false;
|
||||
if (forceLoad) shouldUpdate = true;
|
||||
if (appVersionHasChanged && reloadOnAppVersionChange) shouldUpdate = true;
|
||||
|
||||
if (loadingStatus === 'LOADED' && !shouldUpdate) {
|
||||
return Promise.resolve(resource);
|
||||
} if (loadingStatus === 'LOADING') {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -34,15 +45,19 @@ export function loadAsyncResource ({
|
||||
return reject(); // TODO add reason?
|
||||
});
|
||||
});
|
||||
} if (loadingStatus === 'NOT_LOADED' || forceLoad) {
|
||||
return axios.get(url).then(response => { // TODO support more params
|
||||
} if (loadingStatus === 'NOT_LOADED' || shouldUpdate) { // @TODO set loadingStatus back to LOADING?
|
||||
return axios.get(url).then(response => { // @TODO support more params
|
||||
resource.loadingStatus = 'LOADED';
|
||||
// deserialize can be a promise
|
||||
return Promise.resolve(deserialize(response)).then(deserializedData => {
|
||||
resource.data = deserializedData;
|
||||
// record the app version when the resource was loaded
|
||||
// allows reloading if the app version has changed
|
||||
resource.appVersionOnLoad = store.state.serverAppVersion;
|
||||
return resource;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Invalid loading status "${loadingStatus} for resource at "${path}".`));
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ export function setUpLogging () { // eslint-disable-line import/prefer-default-e
|
||||
|
||||
_LTracker.push({
|
||||
err,
|
||||
vm,
|
||||
info,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -662,8 +662,9 @@ export default {
|
||||
this.selectConversation(this.initiatedConversation.uuid);
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
beforeDestroy () {
|
||||
this.$root.$off(EVENTS.RESYNC_COMPLETED);
|
||||
this.$root.$off(EVENTS.PM_REFRESH);
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import Vue from 'vue';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export async function getChat (store, payload) {
|
||||
@@ -70,8 +71,15 @@ export async function clearFlagCount (store, payload) {
|
||||
}
|
||||
|
||||
export async function markChatSeen (store, payload) {
|
||||
if (store.state.user.newMessages) delete store.state.user.newMessages[payload.groupId];
|
||||
const url = `/api/v4/groups/${payload.groupId}/chat/seen`;
|
||||
const response = await axios.post(url);
|
||||
|
||||
if (store.state.user.data.newMessages[payload.groupId]) {
|
||||
Vue.delete(store.state.user.data.newMessages, payload.groupId);
|
||||
}
|
||||
if (payload.notificationId) {
|
||||
store.state.notificationsRemoved.push(payload.notificationId);
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -74,8 +74,9 @@ export async function join (store, payload) {
|
||||
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
|
||||
|
||||
user.party._id = groupId;
|
||||
|
||||
Analytics.updateUser({ partyID: groupId });
|
||||
// load the party members so that they get shown in the header
|
||||
store.dispatch('party:getMembers');
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
|
||||
@@ -15,7 +15,7 @@ import * as tags from './tags';
|
||||
import * as hall from './hall';
|
||||
import * as shops from './shops';
|
||||
import * as snackbars from './snackbars';
|
||||
import * as worldState from './world-state';
|
||||
import * as worldState from './worldState';
|
||||
|
||||
// Actions should be named as 'actionName' and can be accessed as 'namespace:actionName'
|
||||
// Example: fetch in user.js -> 'user:fetch'
|
||||
|
||||
@@ -3,6 +3,7 @@ import axios from 'axios';
|
||||
export async function readNotification (store, payload) {
|
||||
const url = `/api/v4/notifications/${payload.notificationId}/read`;
|
||||
const response = await axios.post(url);
|
||||
store.state.notificationsRemoved.push(payload.notificationId);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -11,6 +12,7 @@ export async function readNotifications (store, payload) {
|
||||
const response = await axios.post(url, {
|
||||
notificationIds: payload.notificationIds,
|
||||
});
|
||||
store.state.notificationsRemoved.push(...payload.notificationIds);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function getWorldState () { // eslint-disable-line import/prefer-default-export
|
||||
const url = '/api/v4/world-state';
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { loadAsyncResource } from '@/libs/asyncResource';
|
||||
|
||||
export async function getWorldState (store, options = {}) {
|
||||
return loadAsyncResource({
|
||||
store,
|
||||
path: 'worldState',
|
||||
url: '/api/v4/world-state',
|
||||
deserialize (response) {
|
||||
return response.data.data;
|
||||
},
|
||||
forceLoad: options.forceLoad,
|
||||
reloadOnAppVersionChange: true, // reload when the server has been updated
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import * as shops from './shops';
|
||||
import * as tasks from './tasks';
|
||||
import * as party from './party';
|
||||
import * as members from './members';
|
||||
import * as worldState from './worldState';
|
||||
|
||||
// Getters should be named as 'getterName' and can be accessed as 'namespace:getterName'
|
||||
// Example: gems in user.js -> 'user:gems'
|
||||
@@ -14,6 +15,7 @@ const getters = flattenAndNamespace({
|
||||
party,
|
||||
members,
|
||||
shops,
|
||||
worldState,
|
||||
});
|
||||
|
||||
export default getters;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
function getWorldDamage (store) {
|
||||
const worldState = store.state.worldState.data;
|
||||
|
||||
return worldState
|
||||
&& worldState.worldBoss
|
||||
&& worldState.worldBoss.extra
|
||||
&& worldState.worldBoss.extra.worldDmg;
|
||||
}
|
||||
|
||||
export function brokenSeasonalShop (store) {
|
||||
const worldDmg = getWorldDamage(store);
|
||||
return worldDmg && worldDmg.seasonalShop;
|
||||
}
|
||||
|
||||
export function brokenMarket (store) {
|
||||
const worldDmg = getWorldDamage(store);
|
||||
return worldDmg && worldDmg.market;
|
||||
}
|
||||
|
||||
export function brokenQuests (store) {
|
||||
const worldDmg = getWorldDamage(store);
|
||||
return worldDmg && worldDmg.quests;
|
||||
}
|
||||
@@ -50,12 +50,20 @@ export default function () {
|
||||
actions,
|
||||
getters,
|
||||
state: {
|
||||
serverAppVersion: '',
|
||||
serverAppVersion: null,
|
||||
title: 'Habitica',
|
||||
isUserLoggedIn,
|
||||
isUserLoaded: false, // Means the user and the user's tasks are ready
|
||||
// Means the user and the user's tasks are ready
|
||||
// @TODO use store.user.loaded since it's an async resource?
|
||||
isUserLoaded: false,
|
||||
isAmazonReady: false, // Whether the Amazon Payments lib can be used
|
||||
user: asyncResourceFactory(),
|
||||
// Keep track of the ids of notifications that have been removed
|
||||
// to make sure they don't get shown again. It happened due to concurrent requests
|
||||
// which in some cases could result in a read notification showing up again
|
||||
// see https://github.com/HabitRPG/habitica/issues/9242
|
||||
notificationsRemoved: [],
|
||||
worldState: asyncResourceFactory(),
|
||||
credentials: isUserLoggedIn ? {
|
||||
API_ID: AUTH_SETTINGS.auth.apiId,
|
||||
API_TOKEN: AUTH_SETTINGS.auth.apiToken,
|
||||
@@ -65,6 +73,7 @@ export default function () {
|
||||
// in app.vue
|
||||
browserTimezoneOffset,
|
||||
tasks: asyncResourceFactory(), // user tasks
|
||||
// @TODO use asyncresource?
|
||||
completedTodosStatus: 'NOT_LOADED',
|
||||
party: asyncResourceFactory(),
|
||||
partyMembers: asyncResourceFactory(),
|
||||
@@ -105,6 +114,7 @@ export default function () {
|
||||
groupId: '',
|
||||
challengeId: '',
|
||||
group: {},
|
||||
loading: false,
|
||||
},
|
||||
openedItemRows: [],
|
||||
spellOptions: {
|
||||
|
||||
@@ -123,6 +123,56 @@ describe('async resource', () => {
|
||||
expect(axios.get).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe('reloadOnAppVersionChange', async () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = generateStore();
|
||||
store.state.worldState.loadingStatus = 'LOADED';
|
||||
store.state.serverAppVersion = 1;
|
||||
store.state.worldState.appVersionOnLoad = 1;
|
||||
});
|
||||
|
||||
it('load the resource if it is loaded but the appVersion has changed', async () => {
|
||||
store.state.serverAppVersion = 2;
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v4/world-state').returns(Promise.resolve({
|
||||
data: { data: { _id: 1 } },
|
||||
}));
|
||||
|
||||
const resource = await loadAsyncResource({
|
||||
store,
|
||||
path: 'worldState',
|
||||
url: '/api/v4/world-state',
|
||||
reloadOnAppVersionChange: true,
|
||||
deserialize (response) {
|
||||
return response.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
expect(resource).to.equal(store.state.worldState);
|
||||
expect(resource.loadingStatus).to.equal('LOADED');
|
||||
expect(resource.data._id).to.equal(1);
|
||||
expect(axios.get).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('does not load the resource if it is loaded but the appVersion has changed', async () => {
|
||||
sandbox.stub(axios, 'get').returns(Promise.resolve({ data: { data: { _id: 1 } } }));
|
||||
|
||||
const resource = await loadAsyncResource({
|
||||
store,
|
||||
path: 'worldState',
|
||||
url: '/api/v4/world-state',
|
||||
reloadOnAppVersionChange: true,
|
||||
deserialize (response) {
|
||||
return response.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
expect(resource).to.equal(store.state.worldState);
|
||||
expect(axios.get).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send multiple requests if the resource is being loaded', async () => {
|
||||
const store = generateStore();
|
||||
store.state.user.loadingStatus = 'LOADING';
|
||||
|
||||
@@ -78,5 +78,8 @@
|
||||
"achievementHatchedPet": "Излюпи Животно",
|
||||
"viewAchievements": "Постижения",
|
||||
"letsGetStarted": "Нека да започнем!",
|
||||
"onboardingProgress": "<%= percentage %>% напредък"
|
||||
"onboardingProgress": "<%= percentage %>% напредък",
|
||||
"achievementBareNecessitiesModalText": "Завършихте мисиите за Маймуна, Ленивец и Фиданка!",
|
||||
"achievementBareNecessitiesText": "Завършили са всички мисии за Маймуна, Ленивец и Фиданка.",
|
||||
"achievementBareNecessities": "От първа необходимост"
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@
|
||||
"backgroundMedievalKitchenNotes": "Сгответе страшна буря в Средновековна Кухня.",
|
||||
"backgroundMedievalKitchenText": "Средновековна Кухня",
|
||||
"backgrounds022019": "Комплект 57: Открит Февруари 2019",
|
||||
"backgroundAmongGiantAnemonesText": "Измежду Гигантски Актиниѝ",
|
||||
"backgroundAmongGiantAnemonesText": "Сред Гигантски Анемони",
|
||||
"backgroundFlyingOverTropicalIslandsNotes": "Позволете на гледката да ви отнеме дъхът, докату Прелетявате над Тропически Острови.",
|
||||
"backgroundFlyingOverTropicalIslandsText": "Прелет над Тропически Острови",
|
||||
"backgroundLakeWithFloatingLanternsNotes": "Наблюдавайте звездите от фестивалната атмосфера на Езеро с Плуващи Фенери.",
|
||||
@@ -449,5 +449,37 @@
|
||||
"backgrounds032019": "КОМПЛЕКТ 58: Открит Март 2019",
|
||||
"backgroundValentinesDayFeastingHallNotes": "Почуствайте любовта в Свети Валентинска Банкетна Зала.",
|
||||
"backgroundValentinesDayFeastingHallText": "Свети Валентинска Банкетна Зала",
|
||||
"backgroundOldFashionedBakeryNotes": "Насладете се на апетитната миризмата отвън една Старовремна Пекарня."
|
||||
"backgroundOldFashionedBakeryNotes": "Насладете се на апетитната миризмата отвън една Старовремна Пекарня.",
|
||||
"backgrounds122019": "КОМПЛЕКТ 67: декември 2019",
|
||||
"backgroundPotionShopNotes": "Намерете елексир за всяко заболяване в Магазин за Отвари.",
|
||||
"backgroundPotionShopText": "Магазин за Отвари",
|
||||
"backgroundFlyingInAThunderstormNotes": "Гонете Буйна Гръмотевична Буря колкото близко смеете.",
|
||||
"backgroundFlyingInAThunderstormText": "Буйна Гръмотевична Буря",
|
||||
"backgroundFarmersMarketNotes": "Пазарувайте най-свежите храни в Фермерски Пазар.",
|
||||
"backgroundFarmersMarketText": "Фермерски Пазар",
|
||||
"backgrounds112019": "КОМПЛЕКТ 66: ноември 2019",
|
||||
"backgroundMonsterMakersWorkshopNotes": "Експериментирайте с дискредитирани науки в Работилницата на Създател на Чудовища.",
|
||||
"backgroundMonsterMakersWorkshopText": "Работилницата на Създател на Чудовища",
|
||||
"backgroundPumpkinCarriageNotes": "Возете се във вълшебен Тиквен Файтон преди часовника да удари полунощ.",
|
||||
"backgroundPumpkinCarriageText": "Тиквен Файтон",
|
||||
"backgroundFoggyMoorNotes": "Внимавайте къде стъпвате като прекосявате Мъгливо Тресавище.",
|
||||
"backgroundFoggyMoorText": "Мъгливо Тресавище",
|
||||
"backgrounds102019": "КОМПЛЕКТ 65: октомври 2019",
|
||||
"backgroundInAClassroomNotes": "Абсорбирайте знания от вашите ментори в Класна Стая.",
|
||||
"backgroundInAClassroomText": "Класна Стая",
|
||||
"backgroundInAnAncientTombNotes": "Изправете се пред тайните на Древна Гробница.",
|
||||
"backgroundInAnAncientTombText": "Древна Гробница",
|
||||
"backgroundAutumnFlowerGardenNotes": "Поемете топлината на една Есенна Цветна Градина.",
|
||||
"backgroundAutumnFlowerGardenText": "Есенна Цветна Градина",
|
||||
"backgrounds092019": "КОМПЛЕКТ 64: септември 2019",
|
||||
"backgroundTreehouseNotes": "Пошляйте се в дървесно скривалище само за вас, в собствената си Къща на Дърво.",
|
||||
"backgroundTreehouseText": "Къща на Дърво",
|
||||
"backgroundGiantDandelionsNotes": "Шлайте се сред Гигантски Глухарчета.",
|
||||
"backgroundGiantDandelionsText": "Гигантски Глухарчета",
|
||||
"backgroundAmidAncientRuinsNotes": "Застанете в почит към мистериозното минало Сред Древни Руини.",
|
||||
"backgroundAmongGiantAnemonesNotes": "Експлорирайте рифовия живот, защитен от хищници Сред Гигантски Анемони.",
|
||||
"backgroundAmidAncientRuinsText": "Сред Древни Руини",
|
||||
"backgrounds082019": "КОМПЛЕКТ 63: август 2019",
|
||||
"backgroundHolidayMarketNotes": "Намерете перфектните подаръци и декорации в Празничния Пазар.",
|
||||
"backgroundHolidayMarketText": "Празничен Пазар"
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
"allocatePerPop": "Добавяне на точка към Усет",
|
||||
"allocateInt": "Разпределени точки към Интелигентност:",
|
||||
"allocateIntPop": "Добавяне на точка към Интелигентност",
|
||||
"noMoreAllocate": "След като достигнахте ниво 100, вече няма да получавате повече показателни точки. Може да продължавате да вдигате нива или да започнете ново приключение от ниво 1, използвайки <a href='http://habitica.fandom.com/wiki/Orb_of_Rebirth' target='_blank'>Кълбото на прераждането</a>, вече достъпно безплатно на пазара.",
|
||||
"noMoreAllocate": "След като достигнахте ниво 100, вече няма да получавате повече показателни точки. Може да продължавате да вдигате нива или да започнете ново приключение от ниво 1, използвайки <a href='http://habitica.fandom.com/wiki/Orb_of_Rebirth' target='_blank'>Кълбото на прераждането</a>!",
|
||||
"stats": "Показатели",
|
||||
"achievs": "Постижения",
|
||||
"strength": "Сила",
|
||||
@@ -156,7 +156,7 @@
|
||||
"optOutOfClasses": "Отказване",
|
||||
"optOutOfPMs": "Отказване",
|
||||
"chooseClass": "Изберете клас",
|
||||
"chooseClassLearnMarkdown": "[Научете повече относно класовата система на Хабитика](http://habitica.fandom.com/wiki/Class_System)",
|
||||
"chooseClassLearnMarkdown": "[Научете повече относно класовата система на Хабитика](https://habitica.fandom.com/wiki/Class_System)",
|
||||
"optOutOfClassesText": "Не Ви се занимава с класове? Искате да изберете по-късно? Откажете се от тях — ще бъдете воин без специални умения. Можете да прочетете относно класовата система по-късно в уикито, както и да включите класовете по всяко време от Потребителската иконка -> Настройки.",
|
||||
"selectClass": "Избиране на <%= heroClass %>",
|
||||
"select": "Избиране",
|
||||
@@ -224,5 +224,9 @@
|
||||
"mainHand": "Основна ръка",
|
||||
"offHand": "Страничен",
|
||||
"statPoints": "Показателни точки",
|
||||
"pts": "точки"
|
||||
"pts": "точки",
|
||||
"chatCastSpellUser": "<%= username %> използва заклинанието <%= spell %> върху <%= target %>.",
|
||||
"chatCastSpellParty": "<%= username %> използва заклинанието <%= spell %> върху групата.",
|
||||
"purchasePetItemConfirm": "Тази покупка би надвишила броя на предметите, които са ви нужни за да излюпите всички възможни <% = itemText%> любимци. Сигурни ли сте?",
|
||||
"purchaseForGold": "Купуване за <%= cost %> Злато?"
|
||||
}
|
||||
|
||||