Merge branch 'develop' into release

This commit is contained in:
Sabe Jones
2018-02-09 00:58:13 +00:00
71 changed files with 5266 additions and 3876 deletions

View File

@@ -20,7 +20,8 @@ env:
- DISABLE_REQUEST_LOGGING=true
matrix:
- TEST="lint"
- TEST="test:api-v3" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:api-v3:unit" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:api-v3:integration" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:sanity"
- TEST="test:content" COVERAGE=true
- TEST="test:common" COVERAGE=true

View File

@@ -8,7 +8,7 @@ gulp.task('apidoc:clean', (done) => {
clean(APIDOC_DEST_PATH, done);
});
gulp.task('apidoc', ['apidoc:clean'], (done) => {
gulp.task('apidoc', gulp.series('apidoc:clean', (done) => {
let result = apidoc.createDoc({
src: APIDOC_SRC_PATH,
dest: APIDOC_DEST_PATH,
@@ -19,8 +19,8 @@ gulp.task('apidoc', ['apidoc:clean'], (done) => {
} else {
done();
}
});
}));
gulp.task('apidoc:watch', ['apidoc'], () => {
return gulp.watch(`${APIDOC_SRC_PATH}/**/*.js`, ['apidoc']);
});
gulp.task('apidoc:watch', gulp.series('apidoc', (done) => {
return gulp.watch(`${APIDOC_SRC_PATH}/**/*.js`, gulp.series('apidoc', done));
}));

View File

@@ -2,12 +2,6 @@ import gulp from 'gulp';
import babel from 'gulp-babel';
import webpackProductionBuild from '../webpack/build';
gulp.task('build', () => {
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
gulp.start('build:prod');
}
});
gulp.task('build:src', () => {
return gulp.src('website/server/**/*.js')
.pipe(babel())
@@ -20,18 +14,30 @@ gulp.task('build:common', () => {
.pipe(gulp.dest('website/common/transpiled-babel/'));
});
gulp.task('build:server', ['build:src', 'build:common']);
gulp.task('build:server', gulp.series('build:src', 'build:common', done => done()));
// Client Production Build
gulp.task('build:client', (done) => {
webpackProductionBuild((err, output) => {
if (err) return done(err);
console.log(output); // eslint-disable-line no-console
done();
});
});
gulp.task('build:prod', [
gulp.task('build:prod', gulp.series(
'build:server',
'build:client',
'apidoc',
]);
done => done()
));
let buildArgs = [];
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
buildArgs.push('build:prod');
}
gulp.task('build', gulp.series(buildArgs, (done) => {
done();
}));

View File

@@ -1,5 +1,4 @@
import mongoose from 'mongoose';
import autoinc from 'mongoose-id-autoinc';
import logger from '../website/server/libs/logger';
import nconf from 'nconf';
import repl from 'repl';
@@ -25,10 +24,10 @@ let improveRepl = (context) => {
const isProd = nconf.get('NODE_ENV') === 'production';
const mongooseOptions = !isProd ? {} : {
replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
keepAlive: 1,
connectTimeoutMS: 30000,
useMongoClient: true,
};
autoinc.init(
mongoose.connect(
nconf.get('NODE_DB_URI'),
mongooseOptions,
@@ -36,12 +35,12 @@ let improveRepl = (context) => {
if (err) throw err;
logger.info('Connected with Mongoose');
}
)
);
};
gulp.task('console', () => {
gulp.task('console', (done) => {
improveRepl(repl.start({
prompt: 'Habitica > ',
}).context);
done();
});

View File

@@ -7,6 +7,7 @@ import mergeStream from 'merge-stream';
import {basename} from 'path';
import {sync} from 'glob';
import {each} from 'lodash';
import vinylBuffer from 'vinyl-buffer';
// https://github.com/Ensighten/grunt-spritesmith/issues/67#issuecomment-34786248
const MAX_SPRITESHEET_SIZE = 1024 * 1024 * 3;
@@ -104,6 +105,7 @@ function createSpritesStream (name, src) {
}));
let imgStream = spriteData.img
.pipe(vinylBuffer())
.pipe(imagemin())
.pipe(gulp.dest(IMG_DIST_PATH));
@@ -117,8 +119,6 @@ function createSpritesStream (name, src) {
return stream;
}
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
gulp.task('sprites:main', () => {
let mainSrc = sync('website/raw_sprites/spritesmith/**/*.png');
return createSpritesStream('main', mainSrc);
@@ -133,7 +133,7 @@ gulp.task('sprites:clean', (done) => {
clean(`${IMG_DIST_PATH}spritesmith*,${CSS_DIST_PATH}spritesmith*}`, done);
});
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
gulp.task('sprites:checkCompiledDimensions', gulp.series('sprites:main', 'sprites:largeSprites', (done) => {
console.log('Verifiying that images do not exceed max dimensions'); // eslint-disable-line no-console
let numberOfSheetsThatAreTooBig = 0;
@@ -159,4 +159,7 @@ gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSpri
} else {
console.log('All images are within the correct dimensions'); // eslint-disable-line no-console
}
});
done();
}));
gulp.task('sprites:compile', gulp.series('sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions', done => done()));

View File

@@ -3,7 +3,7 @@ import nodemon from 'gulp-nodemon';
let pkg = require('../package.json');
gulp.task('nodemon', () => {
gulp.task('nodemon', (done) => {
nodemon({
script: pkg.main,
ignore: [
@@ -12,4 +12,5 @@ gulp.task('nodemon', () => {
'common/dist/script/content/*',
],
});
done();
});

View File

@@ -4,7 +4,6 @@ import {
import mongoose from 'mongoose';
import { exec } from 'child_process';
import gulp from 'gulp';
import runSequence from 'run-sequence';
import os from 'os';
import nconf from 'nconf';
@@ -39,23 +38,23 @@ let testBin = (string, additionalEnvVariables = '') => {
}
};
gulp.task('test:nodemon', () => {
gulp.task('test:nodemon', gulp.series(function setupNodemon (done) {
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
runSequence('nodemon');
});
done();
}, 'nodemon'));
gulp.task('test:prepare:mongo', (cb) => {
mongoose.connect(TEST_DB_URI, (err) => {
if (err) return cb(`Unable to connect to mongo database. Are you sure it's running? \n\n${err}`);
mongoose.connection.db.dropDatabase();
mongoose.connection.close();
cb();
mongoose.connection.dropDatabase((err2) => {
if (err2) return cb(err2);
mongoose.connection.close(cb);
});
});
});
gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
gulp.task('test:prepare:server', gulp.series('test:prepare:mongo', (done) => {
if (!server) {
server = exec(testBin('node ./website/server/index.js', `NODE_DB_URI=${TEST_DB_URI} PORT=${TEST_SERVER_PORT}`), (error, stdout, stderr) => {
if (error) {
@@ -64,16 +63,18 @@ gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
if (stderr) {
console.error(stderr); // eslint-disable-line no-console
}
done();
});
}
});
}));
gulp.task('test:prepare:build', ['build']);
gulp.task('test:prepare:build', gulp.series('build', done => done()));
gulp.task('test:prepare', [
gulp.task('test:prepare', gulp.series(
'test:prepare:build',
'test:prepare:mongo',
]);
done => done()
));
gulp.task('test:sanity', (cb) => {
let runner = exec(
@@ -88,7 +89,7 @@ gulp.task('test:sanity', (cb) => {
pipe(runner);
});
gulp.task('test:common', ['test:prepare:build'], (cb) => {
gulp.task('test:common', gulp.series('test:prepare:build', (cb) => {
let runner = exec(
testBin(COMMON_TEST_COMMAND),
(err) => {
@@ -99,17 +100,17 @@ gulp.task('test:common', ['test:prepare:build'], (cb) => {
}
);
pipe(runner);
});
}));
gulp.task('test:common:clean', (cb) => {
pipe(exec(testBin(COMMON_TEST_COMMAND), () => cb()));
});
gulp.task('test:common:watch', ['test:common:clean'], () => {
gulp.watch(['common/script/**/*', 'test/common/**/*'], ['test:common:clean']);
});
gulp.task('test:common:watch', gulp.series('test:common:clean', () => {
return gulp.watch(['common/script/**/*', 'test/common/**/*'], gulp.series('test:common:clean', done => done()));
}));
gulp.task('test:common:safe', ['test:prepare:build'], (cb) => {
gulp.task('test:common:safe', gulp.series('test:prepare:build', (cb) => {
let runner = exec(
testBin(COMMON_TEST_COMMAND),
(err, stdout) => { // eslint-disable-line handle-callback-err
@@ -123,9 +124,9 @@ gulp.task('test:common:safe', ['test:prepare:build'], (cb) => {
}
);
pipe(runner);
});
}));
gulp.task('test:content', ['test:prepare:build'], (cb) => {
gulp.task('test:content', gulp.series('test:prepare:build', (cb) => {
let runner = exec(
testBin(CONTENT_TEST_COMMAND),
CONTENT_OPTIONS,
@@ -137,17 +138,17 @@ gulp.task('test:content', ['test:prepare:build'], (cb) => {
}
);
pipe(runner);
});
}));
gulp.task('test:content:clean', (cb) => {
pipe(exec(testBin(CONTENT_TEST_COMMAND), CONTENT_OPTIONS, () => cb()));
});
gulp.task('test:content:watch', ['test:content:clean'], () => {
gulp.watch(['common/script/content/**', 'test/**'], ['test:content:clean']);
});
gulp.task('test:content:watch', gulp.series('test:content:clean', () => {
return gulp.watch(['common/script/content/**', 'test/**'], gulp.series('test:content:clean', done => done()));
}));
gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
gulp.task('test:content:safe', gulp.series('test:prepare:build', (cb) => {
let runner = exec(
testBin(CONTENT_TEST_COMMAND),
CONTENT_OPTIONS,
@@ -162,7 +163,7 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
}
);
pipe(runner);
});
}));
gulp.task('test:api-v3:unit', (done) => {
let runner = exec(
@@ -179,7 +180,7 @@ gulp.task('test:api-v3:unit', (done) => {
});
gulp.task('test:api-v3:unit:watch', () => {
gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], ['test:api-v3:unit']);
return gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api-v3:unit', done => done()));
});
gulp.task('test:api-v3:integration', (done) => {
@@ -198,8 +199,10 @@ gulp.task('test:api-v3:integration', (done) => {
});
gulp.task('test:api-v3:integration:watch', () => {
gulp.watch(['website/server/controllers/api-v3/**/*', 'common/script/ops/*', 'website/server/libs/*.js',
'test/api/v3/integration/**/*'], ['test:api-v3:integration']);
return gulp.watch([
'website/server/controllers/api-v3/**/*', 'common/script/ops/*', 'website/server/libs/*.js',
'test/api/v3/integration/**/*',
], gulp.series('test:api-v3:integration', done => done()));
});
gulp.task('test:api-v3:integration:separate-server', (done) => {
@@ -212,21 +215,17 @@ gulp.task('test:api-v3:integration:separate-server', (done) => {
pipe(runner);
});
gulp.task('test', (done) => {
runSequence(
gulp.task('test', gulp.series(
'test:sanity',
'test:content',
'test:common',
'test:api-v3:unit',
'test:api-v3:integration',
done
);
});
done => done()
));
gulp.task('test:api-v3', (done) => {
runSequence(
gulp.task('test:api-v3', gulp.series(
'test:api-v3:unit',
'test:api-v3:integration',
done
);
});
done => done()
));

View File

@@ -93,9 +93,7 @@ const malformedStringExceptions = {
feedPet: true,
};
gulp.task('transifex', ['transifex:missingFiles', 'transifex:missingStrings', 'transifex:malformedStrings']);
gulp.task('transifex:missingFiles', () => {
gulp.task('transifex:missingFiles', (done) => {
let missingStrings = [];
eachTranslationFile(ALL_LANGUAGES, (error) => {
@@ -109,9 +107,10 @@ gulp.task('transifex:missingFiles', () => {
let formattedMessage = formatMessageForPosting(message, missingStrings);
postToSlack(formattedMessage, SLACK_CONFIG);
}
done();
});
gulp.task('transifex:missingStrings', () => {
gulp.task('transifex:missingStrings', (done) => {
let missingStrings = [];
eachTranslationString(ALL_LANGUAGES, (language, filename, key, englishString, translationString) => {
@@ -126,9 +125,10 @@ gulp.task('transifex:missingStrings', () => {
let formattedMessage = formatMessageForPosting(message, missingStrings);
postToSlack(formattedMessage, SLACK_CONFIG);
}
done();
});
gulp.task('transifex:malformedStrings', () => {
gulp.task('transifex:malformedStrings', (done) => {
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
let interpolationRegex = /<%= [a-zA-Z]* %>/g;
let stringsToLookFor = getStringsWith(jsonFiles, interpolationRegex);
@@ -170,4 +170,11 @@ gulp.task('transifex:malformedStrings', () => {
let formattedMessage = formatMessageForPosting(message, stringsWithIncorrectNumberOfInterpolations);
postToSlack(formattedMessage, SLACK_CONFIG);
}
done();
});
gulp.task(
'transifex',
gulp.series('transifex:missingFiles', 'transifex:missingStrings', 'transifex:malformedStrings'),
(done) => done()
);

View File

@@ -8,10 +8,12 @@
require('babel-register');
const gulp = require('gulp');
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
require('./gulp/gulp-build'); // eslint-disable-line global-require
} else {
require('glob').sync('./gulp/gulp-*').forEach(require); // eslint-disable-line global-require
require('gulp').task('default', ['test']); // eslint-disable-line global-require
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require
}

View File

@@ -18,71 +18,94 @@ var authorUuid = '3e595299-3d8a-4a10-bfe0-88f555e4aa0c'; //... own data is done
*
*/
var dbserver = 'localhost:27017'; // FOR TEST DATABASE
var dbserver = 'username:password@ds031379-a0.mongolab.com:31379'; // FOR PRODUCTION DATABASE
var dbname = 'habitrpg';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var mongo = require('mongoskin');
var _ = require('lodash');
var monk = require('monk');
var dbUsers = monk(connectionString).get('users', { castIds: false });
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'auth.timestamps.loggedin':{$gt:new Date('2016-01-04')}
// '_id': authorUuid // FOR TESTING
};
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
// specify a query to limit the affected users (empty for all users):
var fields = {
'flags.armoireEmpty':1,
'items.gear.owned':1
};
};
// specify user data to change:
var set = {'migration':migrationName, 'flags.armoireEmpty':false};
console.warn('Updating users...');
var progressCount = 1000;
var countSearched = 0;
var countModified = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
return displayData();
if (lastId) {
query._id = {
$gt: lastId
}
countSearched++;
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: {
'flags.armoireEmpty':1,
'items.gear.owned':1
} // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var set = {'migration':migrationName, 'flags.armoireEmpty':false};
if (user.flags.armoireEmpty) {
// this user believes their armoire has no more items in it
if (user.items.gear.owned.weapon_armoire_barristerGavel && user.items.gear.owned.armor_armoire_barristerRobes && user.items.gear.owned.head_armoire_jesterCap && user.items.gear.owned.armor_armoire_jesterCostume && user.items.gear.owned.head_armoire_barristerWig && user.items.gear.owned.weapon_armoire_jesterBaton && user.items.gear.owned.weapon_armoire_lunarSceptre && user.items.gear.owned.armor_armoire_gladiatorArmor && user.items.gear.owned.weapon_armoire_basicCrossbow && user.items.gear.owned.head_armoire_gladiatorHelm && user.items.gear.owned.armor_armoire_lunarArmor && user.items.gear.owned.head_armoire_redHairbow && user.items.gear.owned.head_armoire_violetFloppyHat && user.items.gear.owned.head_armoire_rancherHat && user.items.gear.owned.shield_armoire_gladiatorShield && user.items.gear.owned.head_armoire_blueHairbow && user.items.gear.owned.weapon_armoire_mythmakerSword && user.items.gear.owned.head_armoire_royalCrown && user.items.gear.owned.head_armoire_hornedIronHelm && user.items.gear.owned.weapon_armoire_rancherLasso && user.items.gear.owned.armor_armoire_rancherRobes && user.items.gear.owned.armor_armoire_hornedIronArmor && user.items.gear.owned.armor_armoire_goldenToga && user.items.gear.owned.weapon_armoire_ironCrook && user.items.gear.owned.head_armoire_goldenLaurels && user.items.gear.owned.head_armoire_redFloppyHat && user.items.gear.owned.armor_armoire_plagueDoctorOvercoat && user.items.gear.owned.head_armoire_plagueDoctorHat && user.items.gear.owned.weapon_armoire_goldWingStaff && user.items.gear.owned.head_armoire_yellowHairbow && user.items.gear.owned.eyewear_armoire_plagueDoctorMask && user.items.gear.owned.head_armoire_blackCat && user.items.gear.owned.weapon_armoire_batWand && user.items.gear.owned.head_armoire_orangeCat && user.items.gear.owned.shield_armoire_midnightShield && user.items.gear.owned.armor_armoire_royalRobes && user.items.gear.owned.head_armoire_blueFloppyHat && user.items.gear.owned.shield_armoire_royalCane && user.items.gear.owned.weapon_armoire_shepherdsCrook && user.items.gear.owned.armor_armoire_shepherdRobes && user.items.gear.owned.head_armoire_shepherdHeaddress && user.items.gear.owned.weapon_armoire_blueLongbow && user.items.gear.owned.weapon_armoire_crystalCrescentStaff && user.items.gear.owned.head_armoire_crystalCrescentHat && user.items.gear.owned.armor_armoire_dragonTamerArmor && user.items.gear.owned.head_armoire_dragonTamerHelm && user.items.gear.owned.armor_armoire_crystalCrescentRobes && user.items.gear.owned.shield_armoire_dragonTamerShield && user.items.gear.owned.weapon_armoire_glowingSpear) {
// this user does have all the armoire items so we don't change the flag
// console.log("don't change: " + user._id); // FOR TESTING
}
else {
countModified++;
} else {
// console.log("change: " + user._id); // FOR TESTING
dbUsers.update({_id:user._id}, {$set:set});
dbUsers.update({_id: user._id}, {$set: set});
}
}
else {
} else {
// this user already has armoire marked as containing items to be bought
// so don't change the flag
// console.log("DON'T CHANGE: " + user._id); // FOR TESTING
}
if (countSearched%progressCount == 0) console.warn(countSearched + ' ' + user._id);
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
});
function displayData() {
console.warn('\n' + countSearched + ' users searched\n');
console.warn('\n' + countModified + ' users modified\n');
return exiting(0);
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
@@ -93,3 +116,5 @@ function exiting(code, msg) {
}
process.exit(code);
}
module.exports = processUsers;

7828
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,6 @@
"amplitude": "^2.0.3",
"apidoc": "^0.17.5",
"apn": "^1.7.6",
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.16.0",
@@ -35,7 +34,7 @@
"compression": "^1.6.1",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"cross-env": "^4.0.0",
"cross-env": "^5.1.3",
"css-loader": "^0.28.0",
"csv-stringify": "^1.0.2",
"cwait": "~1.0.1",
@@ -44,22 +43,19 @@
"express-basic-auth": "^1.0.1",
"express-validator": "^2.18.0",
"extract-text-webpack-plugin": "^2.0.0-rc.3",
"glob": "^4.3.5",
"glob": "^7.1.2",
"got": "^6.1.1",
"gulp": "^3.9.0",
"gulp": "^4.0.0",
"gulp-babel": "^6.1.2",
"gulp-imagemin": "^2.4.0",
"gulp-nodemon": "^2.0.4",
"gulp-sourcemaps": "^1.6.0",
"gulp-uglify": "^1.4.2",
"gulp.spritesmith": "^4.1.0",
"gulp-imagemin": "^4.1.0",
"gulp-nodemon": "^2.2.1",
"gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^2.8.1",
"image-size": "~0.3.2",
"image-size": "^0.6.2",
"in-app-purchase": "^1.1.6",
"intro.js": "^2.6.0",
"jade": "~1.11.0",
"jquery": ">=3.0.0",
"js2xmlparser": "~1.0.0",
"lodash": "^4.17.4",
@@ -67,8 +63,7 @@
"method-override": "^2.3.5",
"moment": "^2.13.0",
"moment-recur": "git://github.com/habitrpg/moment-recur.git#f147ef27bbc26ca67638385f3db4a44084c76626",
"mongoose": "^4.8.6",
"mongoose-id-autoinc": "~2013.7.14-4",
"mongoose": "^4.13.10",
"morgan": "^1.7.0",
"nconf": "~0.8.2",
"node-gcm": "^0.14.4",
@@ -84,24 +79,24 @@
"popper.js": "^1.13.0",
"postcss-easy-import": "^2.0.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.0-beta.12",
"pug": "^2.0.0-rc.4",
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
"pusher": "^1.3.0",
"request": "~2.74.0",
"request": "^2.83.0",
"rimraf": "^2.4.3",
"run-sequence": "^1.1.4",
"sass-loader": "^6.0.2",
"shelljs": "^0.7.6",
"shelljs": "^0.8.1",
"stripe": "^4.2.0",
"superagent": "^3.4.3",
"svg-inline-loader": "^0.7.1",
"svg-url-loader": "^2.0.2",
"svgo-loader": "^1.2.1",
"universal-analytics": "~0.3.2",
"universal-analytics": "^0.4.16",
"url-loader": "^0.5.7",
"useragent": "^2.1.9",
"uuid": "^3.0.1",
"validator": "^4.9.0",
"vinyl-buffer": "^1.0.1",
"vue": "^2.5.2",
"vue-loader": "^13.3.0",
"vue-mugen-scroll": "^0.2.1",
@@ -113,7 +108,7 @@
"webpack": "^2.2.1",
"webpack-merge": "^4.0.0",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"winston-loggly-bulk": "^2.0.2",
"xml2js": "^0.4.4"
},
"private": true,
@@ -149,11 +144,11 @@
"babel-plugin-istanbul": "^4.0.0",
"chai": "^3.4.0",
"chai-as-promised": "^5.1.0",
"chalk": "^1.1.3",
"chalk": "^2.3.0",
"chromedriver": "^2.27.2",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^2.11.2",
"cross-spawn": "^5.0.1",
"coveralls": "^3.0.0",
"cross-spawn": "^6.0.4",
"csv": "~0.3.6",
"eslint": "^3.0.0",
"eslint-config-habitrpg": "^3.0.0",
@@ -162,25 +157,23 @@
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-mocha": "^4.7.0",
"eventsource-polyfill": "^0.9.6",
"expect.js": "~0.2.0",
"expect.js": "^0.3.1",
"http-proxy-middleware": "^0.17.0",
"istanbul": "^1.1.0-alpha.1",
"karma": "^1.3.0",
"karma-babel-preprocessor": "^6.0.1",
"karma-chai-plugins": "~0.6.0",
"karma-coverage": "^0.5.3",
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.1.1",
"karma": "^2.0.0",
"karma-babel-preprocessor": "^7.0.0",
"karma-chai-plugins": "^0.9.0",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sinon-chai": "~1.2.0",
"karma-sinon-chai": "^1.3.3",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.24",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^2.0.2",
"lcov-result-merger": "^1.0.2",
"mocha": "^3.2.0",
"mongodb": "^2.2.33",
"mongoskin": "~2.1.0",
"lcov-result-merger": "^2.0.0",
"mocha": "^5.0.0",
"monk": "^4.0.0",
"nightwatch": "^0.9.12",
"phantomjs-prebuilt": "^2.1.12",

View File

@@ -314,5 +314,33 @@ describe('POST /challenges', () => {
groupLeader = await groupLeader.sync();
expect(groupLeader.achievements.joinedChallenge).to.be.true;
});
it('sets summary to challenges name when not supplied', async () => {
const name = 'Test Challenge';
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name,
shortName: 'TC Label',
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.summary).to.eql(name);
});
it('sets summary to challenges', async () => {
const name = 'Test Challenge';
const summary = 'Test Summary Challenge';
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name,
shortName: 'TC Label',
summary,
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.summary).to.eql(summary);
});
});
});

View File

@@ -71,11 +71,9 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
});
it('returns the update chat when previous message parameter is passed and the chat is updated', async () => {
await expect(user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`))
.eventually
.is.an('array')
.to.include(message)
.to.be.lengthOf(1);
let deleteResult = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
expect(deleteResult[0].id).to.eql(message.id);
});
});
});

View File

@@ -234,7 +234,7 @@ describe('POST /chat', () => {
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.be.eql('slur-report-to-mods');
expect(email.sendTxn.args[0][1]).to.eql('slur-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
@@ -287,7 +287,7 @@ describe('POST /chat', () => {
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledThrice;
expect(email.sendTxn.args[2][1]).to.be.eql('slur-report-to-mods');
expect(email.sendTxn.args[2][1]).to.eql('slur-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
@@ -364,6 +364,30 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
});
it('creates a chat with user styles', async () => {
const mount = 'test-mount';
const pet = 'test-pet';
const style = 'test-style';
const userWithStyle = await generateUser({
'items.currentMount': mount,
'items.currentPet': pet,
'preferences.style': style,
});
await userWithStyle.sync();
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.currentMount).to.eql(userWithStyle.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(userWithStyle.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(userWithStyle.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(userWithStyle.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(userWithStyle.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(userWithStyle.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(userWithStyle.preferences.chair);
expect(message.message.userStyles.preferences.background).to.eql(userWithStyle.preferences.background);
});
it('adds backer info to chat', async () => {
const backerInfo = {
npc: 'Town Crier',

View File

@@ -44,6 +44,32 @@ describe('POST /group', () => {
},
});
});
it('sets summary to groups name when not supplied', async () => {
const name = 'Test Group';
const group = await user.post('/groups', {
name,
type: 'guild',
});
const updatedGroup = await user.get(`/groups/${group._id}`);
expect(updatedGroup.summary).to.eql(name);
});
it('sets summary to groups', async () => {
const name = 'Test Group';
const summary = 'Test Summary';
const group = await user.post('/groups', {
name,
type: 'guild',
summary,
});
const updatedGroup = await user.get(`/groups/${group._id}`);
expect(updatedGroup.summary).to.eql(summary);
});
});
context('Guilds', () => {

View File

@@ -264,7 +264,7 @@ describe('POST /groups/:groupId/leave', () => {
it('deletes non existant party from user when user tries to leave', async () => {
let nonExistentPartyId = generateUUID();
let userWithNonExistentParty = await generateUser({'party._id': nonExistentPartyId});
expect(userWithNonExistentParty.party._id).to.be.eql(nonExistentPartyId);
expect(userWithNonExistentParty.party._id).to.eql(nonExistentPartyId);
await expect(userWithNonExistentParty.post(`/groups/${nonExistentPartyId}/leave`))
.to.eventually.be.rejected;

View File

@@ -120,16 +120,16 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
await leader.post(`/groups/${guild._id}/removeMember/${invitedUser._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(invitedUser._id);
expect(email.sendTxn.args[0][1]).to.be.eql('guild-invite-rescinded');
expect(email.sendTxn.args[0][0]._id).to.eql(invitedUser._id);
expect(email.sendTxn.args[0][1]).to.eql('guild-invite-rescinded');
});
it('sends email to removed user', async () => {
await leader.post(`/groups/${guild._id}/removeMember/${member._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(member._id);
expect(email.sendTxn.args[0][1]).to.be.eql('kicked-from-guild');
expect(email.sendTxn.args[0][0]._id).to.eql(member._id);
expect(email.sendTxn.args[0][1]).to.eql('kicked-from-guild');
});
});
@@ -255,16 +255,16 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
await partyLeader.post(`/groups/${party._id}/removeMember/${partyInvitedUser._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(partyInvitedUser._id);
expect(email.sendTxn.args[0][1]).to.be.eql('party-invite-rescinded');
expect(email.sendTxn.args[0][0]._id).to.eql(partyInvitedUser._id);
expect(email.sendTxn.args[0][1]).to.eql('party-invite-rescinded');
});
it('sends email to removed user', async () => {
await partyLeader.post(`/groups/${party._id}/removeMember/${partyMember._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(partyMember._id);
expect(email.sendTxn.args[0][1]).to.be.eql('kicked-from-party');
expect(email.sendTxn.args[0][0]._id).to.eql(partyMember._id);
expect(email.sendTxn.args[0][1]).to.eql('kicked-from-party');
});
});
});

View File

@@ -30,7 +30,7 @@ describe('POST /user/feed/:pet/:food', () => {
data: user.items.pets['Wolf-Base'],
message: t('messageDontEnjoyFood', {
egg: pet.text(),
foodText: food.text(),
foodText: food.textThe(),
}),
});

View File

@@ -131,7 +131,7 @@ describe('errorHandler', () => {
});
it('handle Mongoose Validation errors', () => {
let error = new Error('User validation failed.');
let error = new Error('User validation failed');
error.name = 'ValidationError';
error.errors = {
@@ -151,7 +151,7 @@ describe('errorHandler', () => {
expect(res.json).to.be.calledWith({
success: false,
error: 'BadRequest',
message: 'User validation failed.',
message: 'User validation failed',
errors: [
{ path: 'auth.local.email', message: 'Invalid email.', value: 'not an email' },
],

View File

@@ -125,7 +125,7 @@ describe('shared.ops.feed', () => {
expect(data).to.eql(user.items.pets['Wolf-Base']);
expect(message).to.eql(i18n.t('messageLikesFood', {
egg: pet.text(),
foodText: food.text(),
foodText: food.textThe(),
}));
expect(user.items.food.Meat).to.equal(1);
@@ -143,7 +143,7 @@ describe('shared.ops.feed', () => {
expect(data).to.eql(user.items.pets['Wolf-Spooky']);
expect(message).to.eql(i18n.t('messageLikesFood', {
egg: pet.text(),
foodText: food.text(),
foodText: food.textThe(),
}));
expect(user.items.food.Milk).to.equal(1);
@@ -161,7 +161,7 @@ describe('shared.ops.feed', () => {
expect(data).to.eql(user.items.pets['Wolf-Base']);
expect(message).to.eql(i18n.t('messageDontEnjoyFood', {
egg: pet.text(),
foodText: food.text(),
foodText: food.textThe(),
}));
expect(user.items.food.Milk).to.equal(1);

View File

@@ -291,11 +291,14 @@ describe('shared.ops.scoreTask', () => {
scoreTask({user: ref.afterUser, task: daily, direction: 'up'});
expectGainedPoints(ref.beforeUser, ref.afterUser, freshDaily, daily);
expect(daily.completed).to.eql(true);
expect(daily.history.length).to.eql(1);
});
it('up, down', () => {
scoreTask({user: ref.afterUser, task: daily, direction: 'up'});
expect(daily.history.length).to.eql(1);
scoreTask({user: ref.afterUser, task: daily, direction: 'down'});
expect(daily.history.length).to.eql(0);
expectClosePoints(ref.beforeUser, ref.afterUser, freshDaily, daily);
});

View File

@@ -11,7 +11,7 @@ import * as Tasks from '../../website/server/models/task';
afterEach((done) => {
sandbox.restore();
mongoose.connection.db.dropDatabase(done);
mongoose.connection.dropDatabase(done);
});
export { sleep } from './sleep';

View File

@@ -37,7 +37,7 @@ export async function getProperty (collectionName, id, path) {
// resets the db to an empty state and creates a tavern document
export async function resetHabiticaDB () {
return new Promise((resolve, reject) => {
mongoose.connection.db.dropDatabase((dbErr) => {
mongoose.connection.dropDatabase((dbErr) => {
if (dbErr) return reject(dbErr);
let groups = mongoose.connection.db.collection('groups');
let users = mongoose.connection.db.collection('users');
@@ -119,7 +119,7 @@ before((done) => {
});
after((done) => {
mongoose.connection.db.dropDatabase((err) => {
mongoose.connection.dropDatabase((err) => {
if (err) return done(err);
mongoose.connection.close(done);
});

View File

@@ -5,5 +5,6 @@
--growl
--globals io
-r babel-polyfill
--compilers js:babel-register
--require babel-register
--require ./test/helpers/globals.helper
--exit

View File

@@ -35,7 +35,6 @@ div
div(:class='{sticky: user.preferences.stickyHeader}')
router-view
app-footer
audio#sound(autoplay, ref="sound")
source#oggSource(type="audio/ogg", :src="sound.oggSource")
source#mp3Source(type="audio/mp3", :src="sound.mp3Source")
@@ -83,10 +82,14 @@ div
.container-fluid {
overflow-x: hidden;
flex: 1 0 auto;
}
#app {
height: calc(100% - 56px); /* 56px is the menu */
display: flex;
flex-direction: column;
min-height: 100vh;
}
</style>
@@ -400,7 +403,16 @@ export default {
if (item.purchaseType === 'card') {
this.selectedSpellToBuy = item;
// hide the dialog
this.$root.$emit('bv::hide::modal', 'buy-modal');
// remove the dialog from our modal-stack,
// the default hidden event is delayed
this.$root.$emit('bv::modal::hidden', {
target: {
id: 'buy-modal',
},
});
this.$root.$emit('bv::show::modal', 'select-member-modal');
}
},
@@ -411,6 +423,7 @@ export default {
if (this.selectedSpellToBuy.pinType === 'card') {
const newUserGp = castResult.data.data.user.stats.gp;
this.$store.state.user.data.stats.gp = newUserGp;
this.text(this.$t('sentCardToUser', { profileName: member.profile.name }));
}
this.selectedSpellToBuy = null;

View File

@@ -1,5 +1,5 @@
<template lang="pug">
.row
.row.footer-row
buy-gems-modal(v-if='user')
modify-inventory(v-if="isUserLoaded")
footer.col-12(:class="{expanded: isExpandedFooter}")
@@ -117,6 +117,11 @@
</template>
<style lang="scss" scoped>
.footer-row {
margin: 0;
flex: 0 1 auto;
}
footer {
color: #c3c0c7;
z-index: 17;

View File

@@ -281,7 +281,7 @@ export default {
},
async loadChallenge () {
this.challenge = await this.$store.dispatch('challenges:getChallenge', {challengeId: this.searchId});
this.members = await this.$store.dispatch('members:getChallengeMembers', {challengeId: this.searchId});
this.members = await this.loadMembers({ challengeId: this.searchId, includeAllPublicFields: true });
let tasks = await this.$store.dispatch('tasks:getChallengeTasks', {challengeId: this.searchId});
this.tasksByType = {
habit: [],
@@ -293,6 +293,21 @@ export default {
this.tasksByType[task.type].push(task);
});
},
/**
* Method for loading members of a group, with optional parameters for
* modifying requests.
*
* @param {Object} payload Used for modifying requests for members
*/
loadMembers (payload = null) {
// Remove unnecessary data
if (payload && payload.groupId) {
delete payload.groupId;
}
return this.$store.dispatch('members:getChallengeMembers', payload);
},
editTask (task) {
this.taskFormPurpose = 'edit';
this.editingTask = cloneDeep(task);
@@ -334,7 +349,9 @@ export default {
this.$store.state.memberModalOptions.challengeId = this.challenge._id;
this.$store.state.memberModalOptions.groupId = 'challenge'; // @TODO: change these terrible settings
this.$store.state.memberModalOptions.group = this.group;
this.$store.state.memberModalOptions.memberCount = this.challenge.memberCount;
this.$store.state.memberModalOptions.viewingMembers = this.members;
this.$store.state.memberModalOptions.fetchMoreMembers = this.loadMembers;
this.$root.$emit('bv::show::modal', 'members-modal');
},
async joinChallenge () {

View File

@@ -18,11 +18,9 @@ div
.svg-icon(v-html="icons.like")
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
span(v-if='msg.likes[user._id]') {{ $t('liked') }}
// @TODO make copyAsTodo work in Tavern, guilds, party (inbox can be done later)
span.action(v-if='!inbox', @click='copyAsTodo(msg)')
.svg-icon(v-html="icons.copy")
| {{$t('copyAsTodo')}}
// @TODO make copyAsTodo work in the inbox
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
.svg-icon(v-html="icons.report")
| {{$t('report')}}
@@ -140,7 +138,6 @@ export default {
mixins: [styleHelper],
data () {
return {
copyingMessage: {},
icons: Object.freeze({
like: likeIcon,
copy: copyIcon,
@@ -241,9 +238,7 @@ export default {
this.$emit('messaged-liked', message);
},
copyAsTodo (message) {
// @TODO: Move to Habitica Event
this.copyingMessage = message;
this.$root.$emit('bv::show::modal', 'copyAsTodo');
this.$root.$emit('habitica::copy-as-todo', message);
},
async report () {
this.$root.$emit('habitica::report-chat', {

View File

@@ -11,8 +11,8 @@
.row(v-if='user._id !== msg.uuid')
div(:class='inbox ? "col-4" : "col-2"')
avatar(
v-if='cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected',
:member="cachedProfileData[msg.uuid]",
v-if='msg.userStyles || (cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected)',
:member="msg.userStyles || cachedProfileData[msg.uuid]",
:avatarOnly="true",
:hideClassBadge='true',
@click.native="showMemberModal(msg.uuid)",
@@ -36,8 +36,8 @@
@show-member-modal='showMemberModal')
div(:class='inbox ? "col-4" : "col-2"')
avatar(
v-if='cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected',
:member="cachedProfileData[msg.uuid]",
v-if='msg.userStyles || (cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected)',
:member="msg.userStyles || cachedProfileData[msg.uuid]",
:avatarOnly="true",
:hideClassBadge='true',
@click.native="showMemberModal(msg.uuid)",
@@ -117,12 +117,12 @@ export default {
// @TODO: We need a different lazy load mechnism.
// But honestly, adding a paging route to chat would solve this
messages () {
this.loadProfileCache();
return this.chat;
},
},
watch: {
messages (oldValue, newValue) {
if (newValue.length === oldValue.length) return;
messages () {
this.loadProfileCache();
},
},
@@ -139,18 +139,28 @@ export default {
this._loadProfileCache(screenPosition);
}, 1000),
async _loadProfileCache (screenPosition) {
if (this.loading) return;
this.loading = true;
let promises = [];
const noProfilesLoaded = Object.keys(this.cachedProfileData).length === 0;
// @TODO: write an explination
if (screenPosition && Math.floor(screenPosition) + 1 > this.currentProfileLoadedEnd / 10) {
// @TODO: Remove this after enough messages are cached
if (!noProfilesLoaded && screenPosition && Math.floor(screenPosition) + 1 > this.currentProfileLoadedEnd / 10) {
this.currentProfileLoadedEnd = 10 * (Math.floor(screenPosition) + 1);
} else if (screenPosition) {
} else if (!noProfilesLoaded && screenPosition) {
return;
}
let aboutToCache = {};
this.messages.forEach(message => {
let uuid = message.uuid;
if (message.userStyles) {
this.$set(this.cachedProfileData, uuid, message.userStyles);
}
if (Boolean(uuid) && !this.cachedProfileData[uuid] && !aboutToCache[uuid]) {
if (uuid === 'system' || this.currentProfileLoadedCount === this.currentProfileLoadedEnd) return;
aboutToCache[uuid] = {};
@@ -176,6 +186,8 @@ export default {
this.$set(this.cachedProfileData, uuid, {rejected: true});
}
}
this.loading = false;
},
displayDivider (message) {
if (this.currentDayDividerDisplay !== moment(message.timestamp).day()) {

View File

@@ -1,64 +1,66 @@
<template lang="pug">
b-modal#copyAsTodo(:title="$t('copyMessageAsToDo')", :hide-footer="true", size='md')
.form-group
input.form-control(type='text', v-model='text')
input.form-control(type='text', v-model='task.text')
.form-group
textarea.form-control(rows='5', v-model='notes' focus-element='true')
textarea.form-control(rows='5', v-model='task.notes' focus-element='true')
hr
// @TODO: Implement when tasks are done
//div.task-column.preview
div(v-init='popoverOpen = false', class='task todo uncompleted color-neutral', popover-trigger='mouseenter', data-popover-html="{{popoverOpen ? '' : notes | markdown}}", popover-placement="top")
.task-meta-controls
span(v-if='!obj._locked')
span.task-notes(v-show='notes', @click='popoverOpen = !popoverOpen', popover-trigger='click', data-popover-html="{{notes | markdown}}", popover-placement="top")
span.glyphicon.glyphicon-comment
| &nbsp;
div.task-text
div(v-markdown='text', target='_blank')
task(v-if='task._id', :isUser="isUser", :task="task")
.modal-footer
button.btn.btn-secondary(@click='close()') {{ $t('close') }}
button.btn.btn-primary(@click='saveTodo()') {{ $t('submit') }}
</template>
<script>
import { mapActions } from 'client/libs/store';
import markdownDirective from 'client/directives/markdown';
import notificationsMixin from 'client/mixins/notifications';
import Task from 'client/components/tasks/task';
import taskDefaults from 'common/script/libs/taskDefaults';
const baseUrl = 'https://habitica.com';
export default {
directives: {
markdown: markdownDirective,
},
components: {
Task,
},
mixins: [notificationsMixin],
props: ['copyingMessage', 'groupName', 'groupId'],
data () {
return {
text: '',
notes: '',
isUser: true,
task: {},
};
},
watch: {
copyingMessage () {
this.text = this.copyingMessage.text;
let baseUrl = 'https://habitica.com';
this.notes = `[${this.copyingMessage.user}](${baseUrl}/static/home/#?memberId=${this.copyingMessage.uuid}) wrote in [${this.groupName}](${baseUrl}/groups/guild/${this.groupId})`;
mounted () {
this.$root.$on('habitica::copy-as-todo', message => {
const notes = `${message.user} wrote in [${this.groupName}](${baseUrl}/groups/guild/${this.groupId})`;
const newTask = {
text: message.text,
type: 'todo',
notes,
};
this.task = taskDefaults(newTask);
this.$root.$emit('bv::show::modal', 'copyAsTodo');
});
},
destroyed () {
this.$root.$off('habitica::copy-as-todo');
},
methods: {
...mapActions({
createTask: 'tasks:create',
}),
close () {
this.$root.$emit('bv::hide::modal', 'copyAsTodo');
},
saveTodo () {
// let newTask = {
// text: this.text,
// type: 'todo',
// notes: this.notes,
// };
// @TODO: Add after tasks: User.addTask({body:newTask});
// @TODO: Notification.text(window.env.t('messageAddedAsToDo'));
this.createTask(this.task);
this.text(this.$t('messageAddedAsToDo'));
this.$root.$emit('bv::hide::modal', 'copyAsTodo');
},
},

View File

@@ -40,7 +40,7 @@
community-guidelines
.row
.col-12.hr
chat-message(:chat.sync='group.chat', :group-id='group._id', group-name='group.name')
chat-message(:chat.sync='group.chat', :group-id='group._id', :group-name='group.name')
.col-12.col-sm-4.sidebar
.row(:class='{"guild-background": !isParty}')
.col-12
@@ -375,6 +375,7 @@ export default {
silverGuildBadgeIcon,
bronzeGuildBadgeIcon,
}),
members: [],
selectedQuest: {},
sections: {
quest: true,
@@ -456,14 +457,43 @@ export default {
},
},
methods: {
load () {
this.fetchGuild();
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();
// Fetch group members on load
this.members = await this.loadMembers({
groupId: this.group._id,
includeAllPublicFields: true,
});
this.$root.$on('updatedGroup', group => {
let updatedGroup = extend(this.group, group);
this.$set(this.group, updatedGroup);
});
},
/**
* Method for loading members of a group, with optional parameters for
* modifying requests.
*
* @param {Object} payload Used for modifying requests for members
*/
loadMembers (payload = null) {
// Remove unnecessary data
if (payload && payload.challengeId) {
delete payload.challengeId;
}
return this.$store.dispatch('members:getGroupMembers', payload);
},
// @TODO: abstract autocomplete
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
getCoord (e, text) {
@@ -500,6 +530,9 @@ export default {
showMemberModal () {
this.$store.state.memberModalOptions.groupId = this.group._id;
this.$store.state.memberModalOptions.group = this.group;
this.$store.state.memberModalOptions.memberCount = this.group.memberCount;
this.$store.state.memberModalOptions.viewingMembers = this.members;
this.$store.state.memberModalOptions.fetchMoreMembers = this.loadMembers;
this.$root.$emit('bv::show::modal', 'members-modal');
},
async sendMessage () {

View File

@@ -48,7 +48,7 @@ div
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.removeIcon")
span.text {{$t('removeManager2')}}
.row(v-if='groupId === "challenge"')
.row(v-if='isLoadMoreAvailable')
.col-12.text-center
button.btn.btn-secondary(@click='loadMoreMembers()') {{ $t('loadMore') }}
.row.gradient(v-if='members.length > 3')
@@ -296,6 +296,11 @@ export default {
isAdmin () {
return Boolean(this.user.contributor.admin);
},
isLoadMoreAvailable () {
// Only available if the current length of `members` is less than the
// total size of the Group/Challenge
return this.members.length < this.$store.state.memberModalOptions.memberCount;
},
groupIsSubscribed () {
return this.group.purchased.active;
},
@@ -311,16 +316,6 @@ export default {
sortedMembers () {
let sortedMembers = this.members;
if (this.searchTerm) {
sortedMembers = sortedMembers.filter(member => {
return (
member.profile.name
.toLowerCase()
.indexOf(this.searchTerm.toLowerCase()) !== -1
);
});
}
if (!isEmpty(this.sortOption)) {
// Use the memberlist filtered by searchTerm
sortedMembers = orderBy(sortedMembers, [this.sortOption.param], [this.sortOption.order]);
@@ -337,6 +332,15 @@ export default {
group () {
this.getMembers();
},
// Watches `searchTerm` and if present, performs a `searchMembers` action
// and usual `getMembers` otherwise
searchTerm () {
if (this.searchTerm) {
this.searchMembers(this.searchTerm);
} else {
this.getMembers();
}
},
},
methods: {
sendMessage (member) {
@@ -345,22 +349,24 @@ export default {
userName: member.profile.name,
});
},
async getMembers () {
let groupId = this.groupId;
if (groupId && groupId !== 'challenge') {
let members = await this.$store.dispatch('members:getGroupMembers', {
groupId,
async searchMembers (searchTerm = '') {
this.members = await this.$store.state.memberModalOptions.fetchMoreMembers({
challengeId: this.challengeId,
groupId: this.groupId,
searchTerm,
includeAllPublicFields: true,
});
this.members = members;
},
async getMembers () {
let groupId = this.groupId;
if (groupId && groupId !== 'challenge') {
let invites = await this.$store.dispatch('members:getGroupInvites', {
groupId,
includeAllPublicFields: true,
});
this.invites = invites;
}
if (this.$store.state.memberModalOptions.viewingMembers.length > 0) {
this.members = this.$store.state.memberModalOptions.viewingMembers;
}
@@ -425,9 +431,11 @@ export default {
const lastMember = this.members[this.members.length - 1];
if (!lastMember) return;
let newMembers = await this.$store.dispatch('members:getChallengeMembers', {
let newMembers = await this.$store.state.memberModalOptions.fetchMoreMembers({
challengeId: this.challengeId,
groupId: this.groupId,
lastMemberId: lastMember._id,
includeAllPublicFields: true,
});
this.members = this.members.concat(newMembers);

View File

@@ -41,6 +41,7 @@ div
.grey-progress-bar
.collect-progress-bar(:style="{width: (group.quest.progress.collect[key] / value.count) * 100 + '%'}")
strong {{group.quest.progress.collect[key]}} / {{value.count}}
span.float-right {{parseFloat(user.party.quest.progress.collectedItems) || 0}} items found
.boss-info(v-if='questData.boss')
.row
.col-6

View File

@@ -24,7 +24,7 @@
.row
.hr.col-12
chat-message(:chat.sync='group.chat', :group-id='group._id', group-name='group.name')
chat-message(:chat.sync='group.chat', :group-id='group._id', :group-name='group.name')
.col-12.col-sm-4.sidebar
.section

View File

@@ -160,9 +160,10 @@ export default {
},
openPartyModal () {
if (this.user.party._id) {
// Set the party details for the members-modal component
this.$store.state.memberModalOptions.groupId = this.user.party._id;
// @TODO: do we need to fetch party?
// this.$store.state.memberModalOptions.group = this.$store.state.party;
this.$store.state.memberModalOptions.viewingMembers = this.partyMembers;
this.$store.state.memberModalOptions.group = this.user.party;
this.$root.$emit('bv::show::modal', 'members-modal');
return;
}
@@ -174,9 +175,10 @@ export default {
}
},
},
async created () {
created () {
if (this.user.party && this.user.party._id) {
await this.getPartyMembers(true);
this.$store.state.memberModalOptions.groupId = this.user.party._id;
this.getPartyMembers();
}
},
};

View File

@@ -32,6 +32,7 @@
div.clearfix(slot="modal-footer")
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';

View File

@@ -19,6 +19,7 @@
position: absolute;
left: calc(50% - (16px));
bottom: -($badge-size / 2);
z-index: 1;
}
.svg-icon {

View File

@@ -28,7 +28,7 @@ div
)
slot(name="popoverContent", :item="item")
equipmentAttributesPopover(
v-if="item.purchaseType==='gear'",
v-if="item.purchaseType === 'gear'",
:item="item"
)
div.questPopover(v-else-if="item.purchaseType === 'quests'")
@@ -36,8 +36,8 @@ div
questInfo(:quest="item")
div(v-else)
h4.popover-content-title(v-once) {{ item.text }}
.popover-content-text(v-if="showNotes", v-once) {{ item.notes }}
.popover-content-text(v-if='showNotes && item.key !== "armoire"', v-once) {{ item.notes }}
.popover-content-text(v-if='showNotes && item.key === "armoire"') {{ item.notes }}
div(v-if="item.event") {{ limitedString }}
</template>

View File

@@ -14,7 +14,7 @@ transition(name="fade")
.text.col-7.offset-1
div
| {{message}}
.icon.col-4
.icon.col-4.d-flex.align-items-center
div.svg-icon(v-html="icons.health", v-if='notification.type === "hp"')
div.svg-icon(v-html="icons.gold", v-if='notification.type === "gp"')
div.svg-icon(v-html="icons.star", v-if='notification.type === "xp"')
@@ -24,10 +24,10 @@ transition(name="fade")
.text.col-12
div(v-html='notification.text')
.row(v-if='notification.type === "drop"')
.col-2
.col-3
.icon-item
div(:class='notification.icon')
.text.col-9
.text.col-8
div(v-html='notification.text')
</template>
@@ -150,6 +150,9 @@ export default {
},
computed: {
message () {
if (this.notification.flavorMessage) {
return this.notification.flavorMessage;
}
let localeKey = this.negative === 'negative' ? 'lost' : 'gained';
if (this.notification.type === 'hp') localeKey += 'Health';
if (this.notification.type === 'mp') localeKey += 'Mana';

View File

@@ -617,6 +617,10 @@
}
let groupInvite = '';
if (this.$route.query && this.$route.query.p) {
groupInvite = this.$route.query.p;
}
if (this.$route.query && this.$route.query.groupInvite) {
groupInvite = this.$route.query.groupInvite;
}

View File

@@ -188,22 +188,12 @@ export default {
};
},
mounted () {
this.$root.$on('castEnd', (target, type, $event) => {
this.castEnd(target, type, $event);
});
document.addEventListener('keyup', this.handleKeyUp);
// @TODO: should we abstract the drawer state/local store to a library and mixing combo? We use a similar pattern in equipment
const spellDrawerState = getLocalSetting(CONSTANTS.keyConstants.SPELL_DRAWER_STATE);
if (spellDrawerState === CONSTANTS.valueConstants.DRAWER_CLOSED) {
this.$store.state.spellOptions.spellDrawOpen = false;
}
},
beforeDestroy () {
this.$root.$off('castEnd');
document.removeEventListener('keyup', this.handleKeyUp);
},
computed: {
...mapState({user: 'user.data'}),
openStatus () {
@@ -211,10 +201,6 @@ export default {
},
},
methods: {
handleKeyUp (keyEvent) {
if (keyEvent.keyCode !== 27) return;
this.castCancel();
},
drawerToggled (newState) {
this.$store.state.spellOptions.spellDrawOpen = newState;

View File

@@ -8,7 +8,7 @@
.col-4(v-for="tag in tags")
.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", :value="tag.id", v-model="selectedTags", :id="`tag-${tag.id}`")
label.custom-control-label(:title="tag.name", :for="`tag-${tag.id}`") {{tag.name}}
label.custom-control-label(:title="tag.name", :for="`tag-${tag.id}`", v-markdown="tag.name")
.tags-footer
span.clear-tags(@click="clearTags()") {{$t("clearTags")}}
span.close-tags(@click="close()") {{$t("close")}}
@@ -95,8 +95,13 @@
</style>
<script>
import markdownDirective from 'client/directives/markdown';
export default {
props: ['tags', 'value'],
directives: {
markdown: markdownDirective,
},
data () {
return {
selectedTags: [],

View File

@@ -84,14 +84,13 @@
.svg-icon.challenge.broken(v-html="icons.brokenChallengeIcon", v-if='task.challenge.broken', @click='handleBrokenTask(task)')
.d-flex.align-items-center(v-if="hasTags", :id="`tags-icon-${task._id}`")
.svg-icon.tags(v-html="icons.tags")
#tags-popover
b-popover(
v-if="hasTags",
:target="`tags-icon-${task._id}`",
triggers="hover",
placement="bottom",
container="tags-popover",
)
.tags-popover
.d-flex.align-items-center.tags-container
.tags-popover-title(v-once) {{ `${$t('tags')}:` }}
.tag-label(v-for="tag in getTagsFor(task)") {{tag}}
@@ -455,7 +454,7 @@
}
}
#tags-popover /deep/ {
.tags-popover /deep/ {
.tags-container {
flex-wrap: wrap;
margin-top: -3px;
@@ -759,9 +758,9 @@ export default {
dropNotes = Content.eggs[drop.key].notes();
this.drop(this.$t('messageDropEgg', {dropText, dropNotes}), drop);
} else if (drop.type === 'Food') {
dropText = Content.food[drop.key].text();
dropText = Content.food[drop.key].textA();
dropNotes = Content.food[drop.key].notes();
this.drop(this.$t('messageDropFood', {dropArticle: drop.article, dropText, dropNotes}), drop);
this.drop(this.$t('messageDropFood', {dropText, dropNotes}), drop);
} else if (drop.type === 'Quest') {
// TODO $rootScope.selectedQuest = Content.quests[drop.key];
// $rootScope.openModal('questDrop', {controller:'PartyCtrl', size:'sm'});

View File

@@ -143,7 +143,7 @@
.tags-none {{$t('none')}}
.dropdown-toggle
span.category-select(v-else)
.category-label(v-for='tagName in truncatedSelectedTags', :title="tagName") {{ tagName }}
.category-label(v-for='tagName in truncatedSelectedTags', :title="tagName", v-markdown='tagName')
.tags-more(v-if='remainingSelectedTags.length > 0') +{{ $t('more', { count: remainingSelectedTags.length }) }}
.dropdown-toggle
tags-popup(v-if="showTagsSelect", :tags="user.tags", v-model="task.tags", @close='closeTagsPopup()')
@@ -346,7 +346,7 @@
position: relative;
label {
margin-bottom: 8px;
max-height: 30px;
}
}
@@ -637,6 +637,7 @@
<script>
import TagsPopup from './tagsPopup';
import { mapGetters, mapActions, mapState } from 'client/libs/store';
import markdownDirective from 'client/directives/markdown';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import clone from 'lodash/clone';
import Datepicker from 'vuejs-datepicker';
@@ -664,6 +665,9 @@ export default {
toggleSwitch,
draggable,
},
directives: {
markdown: markdownDirective,
},
// purpose is either create or edit, task is the task created or edited
props: ['task', 'purpose', 'challengeId', 'groupId'],
data () {

View File

@@ -239,11 +239,13 @@ export default {
user: message.user,
uuid: message.uuid,
id: message.id,
contributor: message.contributor,
};
if (message.sent) {
newMessage.user = this.user.profile.name;
newMessage.uuid = this.user._id;
newMessage.contributor = this.user.contributor;
}
if (newMessage.text) conversations[userId].messages.push(newMessage);
@@ -306,6 +308,7 @@ export default {
timestamp: new Date(),
user: this.user.profile.name,
uuid: this.user._id,
contributor: this.user.contributor,
});
this.activeChat = convoFound.messages;

View File

@@ -4,6 +4,10 @@ import isArray from 'lodash/isArray';
// @TODO: Let's separate some of the business logic out of Vue if possible
export default {
methods: {
handleCastCancelKeyUp (keyEvent) {
if (keyEvent.keyCode !== 27) return;
this.castCancel();
},
async castStart (spell) {
if (this.$store.state.spellOptions.castingSpell) {
this.castCancel();
@@ -47,6 +51,13 @@ export default {
return true;
});
this.castEnd(tasks, spell.target);
} else {
// If the cast target has to be selected (and can be cancelled)
document.addEventListener('keyup', this.handleCastCancelKeyUp);
this.$root.$on('castEnd', (target, type, $event) => {
this.castEnd(target, type, $event);
});
}
},
async castEnd (target, type) {
@@ -61,17 +72,12 @@ export default {
if (target && target.challenge && target.challenge.id) return this.text(this.$t('invalidTarget'));
if (target && target.group && target.group.id) return this.text(this.$t('invalidTarget'));
// @TODO: just call castCancel?
this.$store.state.spellOptions.castingSpell = false;
this.potionClickMode = false;
this.spell.cast(this.user, target);
// User.save(); // @TODO:
let spell = this.spell;
let targetId = target ? target._id : null;
this.spell = null;
this.applyingAction = false;
this.castCancel();
let spellUrl = `/api/v3/user/class/cast/${spell.key}`;
if (targetId) spellUrl += `?targetId=${targetId}`;
@@ -123,6 +129,10 @@ export default {
this.spell = null;
document.querySelector('body').style.cursor = 'initial';
this.$store.state.spellOptions.castingSpell = false;
// Remove listeners
this.$root.$off('castEnd');
document.removeEventListener('keyup', this.handleCastCancelKeyUp);
},
},
};

View File

@@ -7,15 +7,21 @@ let apiV3Prefix = '/api/v3';
export async function getGroupMembers (store, payload) {
let url = `${apiV3Prefix}/groups/${payload.groupId}/members`;
const params = {};
if (payload.includeAllPublicFields) {
url += '?includeAllPublicFields=true';
params.includeAllPublicFields = true;
}
if (payload.lastMemberId) {
params.lastId = payload.lastMemberId;
}
if (payload.searchTerm) {
url += `?search=${payload.searchTerm}`;
params.search = payload.searchTerm;
}
let response = await axios.get(url);
let response = await axios.get(url, { params });
return response.data.data;
}
@@ -35,17 +41,23 @@ export async function getGroupInvites (store, payload) {
}
export async function getChallengeMembers (store, payload) {
let url = `${apiV3Prefix}/challenges/${payload.challengeId}/members?includeAllPublicFields=true`;
let url = `${apiV3Prefix}/challenges/${payload.challengeId}/members`;
const params = {};
if (payload.includeAllPublicFields) {
params.includeAllPublicFields = true;
}
if (payload.lastMemberId) {
url += `&lastId=${payload.lastMemberId}`;
params.lastId = payload.lastMemberId;
}
if (payload.searchTerm) {
url += `&search=${payload.searchTerm}`;
params.search = payload.searchTerm;
}
let response = await axios.get(url);
let response = await axios.get(url, { params });
return response.data.data;
}

View File

@@ -14,8 +14,16 @@ import { getDropClass } from 'client/libs/notifications';
export function buyItem (store, params) {
const quantity = params.quantity || 1;
const user = store.state.user.data;
const userPinned = user.pinnedItems.slice();
let opResult = buyOp(user, {params, quantity});
// @TODO: Currently resetting the pinned items will reset the market. Purchasing some items does not reset pinned.
// For now, I've added this hack for items like contributor gear to update while I am working on add more computed
// properties to the market. We will use this quick fix while testing the other changes.
user.pinnedItems = userPinned;
return {
result: opResult,
httpCall: axios.post(`/api/v3/user/buy/${params.key}`),
@@ -51,6 +59,7 @@ async function buyArmoire (store, params) {
if (buyResult) {
const resData = buyResult;
const item = resData.armoire;
const message = result.data.message;
const isExperience = item.type === 'experience';
if (item.type === 'gear') {
@@ -59,12 +68,22 @@ async function buyArmoire (store, params) {
store.state.user.data.stats.gp -= armoire.value;
// @TODO: We might need to abstract notifications to library rather than mixin
const notificationOptions = isExperience ?
{
text: `+ ${item.value}`,
type: 'xp',
flavorMessage: message,
} :
{
text: message,
type: 'drop',
icon: getDropClass({type: item.type, key: item.dropKey}),
};
store.dispatch('snackbars:add', {
title: '',
text: isExperience ? item.value : item.dropText,
type: isExperience ? 'xp' : 'drop',
icon: isExperience ? null : getDropClass({type: item.type, key: item.dropKey}),
timeout: true,
...notificationOptions,
});
}
}

View File

@@ -261,37 +261,97 @@
"premiumPotionAddlNotes": "Not usable on quest pet eggs.",
"foodMeat": "Meat",
"foodMeatThe": "the Meat",
"foodMeatA": "Meat",
"foodMilk": "Milk",
"foodMilkThe": "the Milk",
"foodMilkA": "Milk",
"foodPotatoe": "Potato",
"foodPotatoeThe": "the Potato",
"foodPotatoeA": "a Potato",
"foodStrawberry": "Strawberry",
"foodStrawberryThe": "the Strawberry",
"foodStrawberryA": "a Strawberry",
"foodChocolate": "Chocolate",
"foodChocolateThe": "the Chocolate",
"foodChocolateA": "Chocolate",
"foodFish": "Fish",
"foodFishThe": "the Fish",
"foodFishA": "a Fish",
"foodRottenMeat": "Rotten Meat",
"foodRottenMeatThe": "the Rotten Meat",
"foodRottenMeatA": "Rotten Meat",
"foodCottonCandyPink": "Pink Cotton Candy",
"foodCottonCandyPinkThe": "the Pink Cotton Candy",
"foodCottonCandyPinkA": "Pink Cotton Candy",
"foodCottonCandyBlue": "Blue Cotton Candy",
"foodCottonCandyBlueThe": "the Blue Cotton Candy",
"foodCottonCandyBlueA": "Blue Cotton Candy",
"foodHoney": "Honey",
"foodHoneyThe": "the Honey",
"foodHoneyA": "Honey",
"foodCakeSkeleton": "Bare Bones Cake",
"foodCakeSkeletonThe": "the Bare Bones Cake",
"foodCakeSkeletonA": "a Bare Bones Cake",
"foodCakeBase": "Basic Cake",
"foodCakeBaseThe": "the Basic Cake",
"foodCakeBaseA": "a Basic Cake",
"foodCakeCottonCandyBlue": "Candy Blue Cake",
"foodCakeCottonCandyBlueThe": "the Candy Blue Cake",
"foodCakeCottonCandyBlueA": "a Candy Blue Cake",
"foodCakeCottonCandyPink": "Candy Pink Cake",
"foodCakeCottonCandyPinkThe": "the Candy Pink Cake",
"foodCakeCottonCandyPinkA": "a Candy Pink Cake",
"foodCakeShade": "Chocolate Cake",
"foodCakeShadeThe": "the Chocolate Cake",
"foodCakeShadeA": "a Chocolate Cake",
"foodCakeWhite": "Cream Cake",
"foodCakeWhiteThe": "the Cream Cake",
"foodCakeWhiteA": "a Cream Cake",
"foodCakeGolden": "Honey Cake",
"foodCakeGoldenThe": "the Honey Cake",
"foodCakeGoldenA": "a Honey Cake",
"foodCakeZombie": "Rotten Cake",
"foodCakeZombieThe": "the Rotten Cake",
"foodCakeZombieA": "a Rotten Cake",
"foodCakeDesert": "Sand Cake",
"foodCakeDesertThe": "the Sand Cake",
"foodCakeDesertA": "a Sand Cake",
"foodCakeRed": "Strawberry Cake",
"foodCakeRedThe": "the Strawberry Cake",
"foodCakeRedA": "a Strawberry Cake",
"foodCandySkeleton": "Bare Bones Candy",
"foodCandySkeletonThe": "the Bare Bones Candy",
"foodCandySkeletonA": "Bare Bones Candy",
"foodCandyBase": "Basic Candy",
"foodCandyBaseThe": "the Basic Candy",
"foodCandyBaseA": "Basic Candy",
"foodCandyCottonCandyBlue": "Sour Blue Candy",
"foodCandyCottonCandyBlueThe": "the Sour Blue Candy",
"foodCandyCottonCandyBlueA": "Sour Blue Candy",
"foodCandyCottonCandyPink": "Sour Pink Candy",
"foodCandyCottonCandyPinkThe": "the Sour Pink Candy",
"foodCandyCottonCandyPinkA": "Sour Pink Candy",
"foodCandyShade": "Chocolate Candy",
"foodCandyShadeThe": "the Chocolate Candy",
"foodCandyShadeA": "Chocolate Candy",
"foodCandyWhite": "Vanilla Candy",
"foodCandyWhiteThe": "the Vanilla Candy",
"foodCandyWhiteA": "Vanilla Candy",
"foodCandyGolden": "Honey Candy ",
"foodCandyGoldenThe": "the Honey Candy",
"foodCandyGoldenA": "Honey Candy",
"foodCandyZombie": "Rotten Candy",
"foodCandyZombieThe": "the Rotten Candy",
"foodCandyZombieA": "Rotten Candy",
"foodCandyDesert": "Sand Candy",
"foodCandyDesertThe": "the Sand Candy",
"foodCandyDesertA": "Sand Candy",
"foodCandyRed": "Cinnamon Candy",
"foodCandyRedThe": "the Cinnamon Candy",
"foodCandyRedA": "Cinnamon Candy",
"foodSaddleText": "Saddle",
"foodSaddleNotes": "Instantly raises one of your pets into a mount.",

View File

@@ -128,7 +128,7 @@
"weaponSpecialSkiText": "Ski-sassin Pole",
"weaponSpecialSkiNotes": "A weapon capable of destroying hordes of enemies! It also helps the user make very nice parallel turns. Increases Strength by <%= str %>. Limited Edition 2013-2014 Winter Gear.",
"weaponSpecialCandycaneText": "Candy Cane Staff",
"weaponSpecialCandycaneNotes": "A powerful mage's staff. Powerfully DELICIOUS, we mean! Two-handed weapon. Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2013-2014 Winter Gear.",
"weaponSpecialCandycaneNotes": "A powerful mage's staff. Powerfully DELICIOUS, we mean! Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2013-2014 Winter Gear.",
"weaponSpecialSnowflakeText": "Snowflake Wand",
"weaponSpecialSnowflakeNotes": "This wand sparkles with unlimited healing power. Increases Intelligence by <%= int %>. Limited Edition 2013-2014 Winter Gear.",
@@ -1591,5 +1591,6 @@
"eyewearMystery301703Notes": "Perfect for a fancy masquerade or for stealthily moving through a particularly well-dressed crowd. Confers no benefit. March 3017 Subscriber Item.",
"eyewearArmoirePlagueDoctorMaskText": "Plague Doctor Mask",
"eyewearArmoirePlagueDoctorMaskNotes": "An authentic mask worn by the doctors who battle the Plague of Procrastination. Confers no benefit. Enchanted Armoire: Plague Doctor Set (Item 2 of 3)."
"eyewearArmoirePlagueDoctorMaskNotes": "An authentic mask worn by the doctors who battle the Plague of Procrastination. Confers no benefit. Enchanted Armoire: Plague Doctor Set (Item 2 of 3).",
"twoHandedItem": "Two-handed item."
}

View File

@@ -173,6 +173,8 @@
"achievementBewilderText": "Helped defeat the Be-Wilder during the 2016 Spring Fling Event!",
"checkOutProgress": "Check out my progress in Habitica!",
"cards": "Cards",
"sentCardToUser": "You sent a card to <%= profileName %>",
"cardReceivedFrom": "<%= cardType %> from <%= userName %>",
"cardReceived": "You received a <span class=\"notification-bold-blue\"><%= card %></span>",
"greetingCard": "Greeting Card",
"greetingCardExplanation": "You both receive the Cheery Chum achievement!",

View File

@@ -9,8 +9,8 @@
"messageCannotFeedPet": "Can't feed this pet.",
"messageAlreadyMount": "You already have that mount. Try feeding another pet.",
"messageEvolve": "You have tamed <%= egg %>, let's go for a ride!",
"messageLikesFood": "<%= egg %> really likes the <%= foodText %>!",
"messageDontEnjoyFood": "<%= egg %> eats the <%= foodText %> but doesn't seem to enjoy it.",
"messageLikesFood": "<%= egg %> really likes <%= foodText %>!",
"messageDontEnjoyFood": "<%= egg %> eats <%= foodText %> but doesn't seem to enjoy it.",
"messageBought": "Bought <%= itemText %>",
"messageEquipped": " <%= itemText %> equipped.",
"messageUnEquipped": "<%= itemText %> unequipped.",
@@ -21,7 +21,7 @@
"messageNotEnoughGold": "Not Enough Gold",
"messageTwoHandedEquip": "Wielding <%= twoHandedText %> takes two hands, so <%= offHandedText %> has been unequipped.",
"messageTwoHandedUnequip": "Wielding <%= twoHandedText %> takes two hands, so it was unequipped when you armed yourself with <%= offHandedText %>.",
"messageDropFood": "You've found <%= dropArticle %><%= dropText %>!",
"messageDropFood": "You've found <%= dropText %>!",
"messageDropEgg": "You've found a <%= dropText %> Egg!",
"messageDropPotion": "You've found a <%= dropText %> Hatching Potion!",
"messageDropQuest": "You've found a quest!",
@@ -35,7 +35,7 @@
"messageHealthAlreadyMin": "Oh no! You have already run out of health so it's too late to buy a health potion, but don't worry - you can revive!",
"armoireEquipment": "<%= image %> You found a piece of rare Equipment in the Armoire: <%= dropText %>! Awesome!",
"armoireFood": "<%= image %> You rummage in the Armoire and find <%= dropArticle %><%= dropText %>. What's that doing in here?",
"armoireFood": "<%= image %> You rummage in the Armoire and find <%= dropText %>. What's that doing in here?",
"armoireExp": "You wrestle with the Armoire and gain Experience. Take that!",
"messageInsufficientGems": "Not enough gems!",

View File

@@ -81,7 +81,7 @@
"petNotOwned": "You do not own this pet.",
"mountNotOwned": "You do not own this mount.",
"earnedCompanion": "With all your productivity, you've earned a new companion. Feed it to make it grow!",
"feedPet": "Feed <%= article %><%= text %> to your <%= name %>?",
"feedPet": "Feed <%= text %> to your <%= name %>?",
"useSaddle": "Saddle <%= pet %>?",
"raisedPet": "You grew your <%= pet %>!",
"earnedSteed": "By completing your tasks, you've earned a faithful steed!",

View File

@@ -91,7 +91,7 @@ export const ITEM_LIST = {
premiumHatchingPotions: { localeKey: 'hatchingPotion', isEquipment: false },
eggs: { localeKey: 'eggSingular', isEquipment: false },
quests: { localeKey: 'quest', isEquipment: false },
food: { localeKey: 'foodText', isEquipment: false },
food: { localeKey: 'foodTextThe', isEquipment: false },
Saddle: { localeKey: 'foodSaddleText', isEquipment: false },
bundles: { localeKey: 'discountBundle', isEquipment: false },
};

View File

@@ -1,3 +1,5 @@
import t from '../translation';
import {weapon as baseWeapon} from './sets/base';
import {weapon as healerWeapon} from './sets/healer';
@@ -22,4 +24,43 @@ let weapon = {
armoire: armoireWeapon,
};
// Add Two Handed message to all weapons
const rtlLanguages = [
'ae', /* Avestan */
'ar', /* 'العربية', Arabic */
'arc', /* Aramaic */
'bcc', /* 'بلوچی مکرانی', Southern Balochi */
'bqi', /* 'بختياري', Bakthiari */
'ckb', /* 'Soranî / کوردی', Sorani */
'dv', /* Dhivehi */
'fa', /* 'فارسی', Persian */
'glk', /* 'گیلکی', Gilaki */
'he', /* 'עברית', Hebrew */
'ku', /* 'Kurdî / كوردی', Kurdish */
'mzn', /* 'مازِرونی', Mazanderani */
'nqo', /* N'Ko */
'pnb', /* 'پنجابی', Western Punjabi */
'ps', /* 'پښتو', Pashto, */
'sd', /* 'سنڌي', Sindhi */
'ug', /* 'Uyghurche / ئۇيغۇرچە', Uyghur */
'ur', /* 'اردو', Urdu */
'yi', /* 'ייִדיש', Yiddish */
];
for (let key in weapon) {
const set = weapon[key];
for (let weaponKey in set) {
const item = set[weaponKey];
const oldnotes = item.notes;
item.notes = (lang) => {
const twoHandedText = item.twoHanded ? t('twoHandedItem')(lang) : '';
if (rtlLanguages.indexOf(lang) !== -1) {
return `${twoHandedText} ${oldnotes(lang)}`;
}
return `${oldnotes(lang)} ${twoHandedText}`;
};
}
}
module.exports = weapon;

View File

@@ -276,8 +276,9 @@ let canDropCakeFood = false;
api.food = {
Meat: {
text: t('foodMeat'),
textA: t('foodMeatA'),
textThe: t('foodMeatThe'),
target: 'Base',
article: '',
canBuy () {
return canBuyNormalFood;
},
@@ -285,8 +286,9 @@ api.food = {
},
Milk: {
text: t('foodMilk'),
textA: t('foodMilkA'),
textThe: t('foodMilkThe'),
target: 'White',
article: '',
canBuy () {
return canBuyNormalFood;
},
@@ -294,8 +296,9 @@ api.food = {
},
Potatoe: {
text: t('foodPotatoe'),
textA: t('foodPotatoeA'),
textThe: t('foodPotatoeThe'),
target: 'Desert',
article: 'a ',
canBuy () {
return canBuyNormalFood;
},
@@ -303,8 +306,9 @@ api.food = {
},
Strawberry: {
text: t('foodStrawberry'),
textA: t('foodStrawberryA'),
textThe: t('foodStrawberryThe'),
target: 'Red',
article: 'a ',
canBuy () {
return canBuyNormalFood;
},
@@ -312,8 +316,9 @@ api.food = {
},
Chocolate: {
text: t('foodChocolate'),
textA: t('foodChocolateA'),
textThe: t('foodChocolateThe'),
target: 'Shade',
article: '',
canBuy () {
return canBuyNormalFood;
},
@@ -321,8 +326,9 @@ api.food = {
},
Fish: {
text: t('foodFish'),
textA: t('foodFishA'),
textThe: t('foodFishThe'),
target: 'Skeleton',
article: 'a ',
canBuy () {
return canBuyNormalFood;
},
@@ -330,8 +336,9 @@ api.food = {
},
RottenMeat: {
text: t('foodRottenMeat'),
textA: t('foodRottenMeatA'),
textThe: t('foodRottenMeatThe'),
target: 'Zombie',
article: '',
canBuy () {
return canBuyNormalFood;
},
@@ -339,8 +346,9 @@ api.food = {
},
CottonCandyPink: {
text: t('foodCottonCandyPink'),
textA: t('foodCottonCandyPinkA'),
textThe: t('foodCottonCandyPinkThe'),
target: 'CottonCandyPink',
article: '',
canBuy () {
return canBuyNormalFood;
},
@@ -348,8 +356,9 @@ api.food = {
},
CottonCandyBlue: {
text: t('foodCottonCandyBlue'),
textA: t('foodCottonCandyBlueA'),
textThe: t('foodCottonCandyBlueThe'),
target: 'CottonCandyBlue',
article: '',
canBuy () {
return canBuyNormalFood;
},
@@ -357,8 +366,9 @@ api.food = {
},
Honey: {
text: t('foodHoney'),
textA: t('foodHoneyA'),
textThe: t('foodHoneyThe'),
target: 'Golden',
article: '',
canBuy () {
return canBuyNormalFood;
},
@@ -376,8 +386,9 @@ api.food = {
/* eslint-disable camelcase */
Cake_Skeleton: {
text: t('foodCakeSkeleton'),
textA: t('foodCakeSkeletonA'),
textThe: t('foodCakeSkeletonThe'),
target: 'Skeleton',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -385,8 +396,9 @@ api.food = {
},
Cake_Base: {
text: t('foodCakeBase'),
textA: t('foodCakeBaseA'),
textThe: t('foodCakeBaseThe'),
target: 'Base',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -394,8 +406,9 @@ api.food = {
},
Cake_CottonCandyBlue: {
text: t('foodCakeCottonCandyBlue'),
textA: t('foodCakeCottonCandyBlueA'),
textThe: t('foodCakeCottonCandyBlueThe'),
target: 'CottonCandyBlue',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -403,8 +416,9 @@ api.food = {
},
Cake_CottonCandyPink: {
text: t('foodCakeCottonCandyPink'),
textA: t('foodCakeCottonCandyPinkA'),
textThe: t('foodCakeCottonCandyPinkThe'),
target: 'CottonCandyPink',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -412,8 +426,9 @@ api.food = {
},
Cake_Shade: {
text: t('foodCakeShade'),
textA: t('foodCakeShadeA'),
textThe: t('foodCakeShadeThe'),
target: 'Shade',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -421,8 +436,9 @@ api.food = {
},
Cake_White: {
text: t('foodCakeWhite'),
textA: t('foodCakeWhiteA'),
textThe: t('foodCakeWhiteThe'),
target: 'White',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -430,8 +446,9 @@ api.food = {
},
Cake_Golden: {
text: t('foodCakeGolden'),
textA: t('foodCakeGoldenA'),
textThe: t('foodCakeGoldenThe'),
target: 'Golden',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -439,8 +456,9 @@ api.food = {
},
Cake_Zombie: {
text: t('foodCakeZombie'),
textA: t('foodCakeZombieA'),
textThe: t('foodCakeZombieThe'),
target: 'Zombie',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -448,8 +466,9 @@ api.food = {
},
Cake_Desert: {
text: t('foodCakeDesert'),
textA: t('foodCakeDesertA'),
textThe: t('foodCakeDesertThe'),
target: 'Desert',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -457,8 +476,9 @@ api.food = {
},
Cake_Red: {
text: t('foodCakeRed'),
textA: t('foodCakeRedA'),
textThe: t('foodCakeRedThe'),
target: 'Red',
article: '',
canBuy () {
return canBuyCakeFood;
},
@@ -466,8 +486,9 @@ api.food = {
},
Candy_Skeleton: {
text: t('foodCandySkeleton'),
textA: t('foodCandySkeletonA'),
textThe: t('foodCandySkeletonThe'),
target: 'Skeleton',
article: '',
canBuy () {
return canBuyCandyFood;
},
@@ -475,8 +496,9 @@ api.food = {
},
Candy_Base: {
text: t('foodCandyBase'),
textA: t('foodCandyBaseA'),
textThe: t('foodCandyBaseThe'),
target: 'Base',
article: '',
canBuy () {
return canBuyCandyFood;
},
@@ -484,8 +506,9 @@ api.food = {
},
Candy_CottonCandyBlue: {
text: t('foodCandyCottonCandyBlue'),
textA: t('foodCandyCottonCandyBlueA'),
textThe: t('foodCandyCottonCandyBlueThe'),
target: 'CottonCandyBlue',
article: '',
canBuy () {
return canBuyCandyFood;
},
@@ -493,8 +516,9 @@ api.food = {
},
Candy_CottonCandyPink: {
text: t('foodCandyCottonCandyPink'),
textA: t('foodCandyCottonCandyPinkA'),
textThe: t('foodCandyCottonCandyPinkThe'),
target: 'CottonCandyPink',
article: '',
canBuy () {
return canBuyCandyFood;
},
@@ -502,8 +526,9 @@ api.food = {
},
Candy_Shade: {
text: t('foodCandyShade'),
textA: t('foodCandyShadeA'),
textThe: t('foodCandyShadeThe'),
target: 'Shade',
article: '',
canBuy () {
return canBuyCandyFood;
},
@@ -511,8 +536,9 @@ api.food = {
},
Candy_White: {
text: t('foodCandyWhite'),
textA: t('foodCandyWhiteA'),
textThe: t('foodCandyWhiteThe'),
target: 'White',
article: '',
canBuy () {
return canBuyCandyFood;
},
@@ -520,8 +546,9 @@ api.food = {
},
Candy_Golden: {
text: t('foodCandyGolden'),
textA: t('foodCandyGoldenA'),
textThe: t('foodCandyGoldenThe'),
target: 'Golden',
article: '',
canBuy () {
return canBuyCandyFood;
},
@@ -529,8 +556,9 @@ api.food = {
},
Candy_Zombie: {
text: t('foodCandyZombie'),
textA: t('foodCandyZombieA'),
textThe: t('foodCandyZombieThe'),
target: 'Zombie',
article: '',
canBuy () {
return canBuyCandyFood;
},
@@ -538,8 +566,9 @@ api.food = {
},
Candy_Desert: {
text: t('foodCandyDesert'),
textA: t('foodCandyDesertA'),
textThe: t('foodCandyDesertThe'),
target: 'Desert',
article: '',
canBuy () {
return canBuyCandyFood;
},
@@ -547,8 +576,9 @@ api.food = {
},
Candy_Red: {
text: t('foodCandyRed'),
textA: t('foodCandyRedA'),
textThe: t('foodCandyRedThe'),
target: 'Red',
article: '',
canBuy () {
return canBuyCandyFood;
},

View File

@@ -77,8 +77,7 @@ module.exports = function randomDrop (user, options, req = {}, analytics) {
user.items.food[drop.key] += 1;
drop.type = 'Food';
drop.dialog = i18n.t('messageDropFood', {
dropArticle: drop.article,
dropText: drop.text(req.language),
dropText: drop.textA(req.language),
dropNotes: drop.notes(req.language),
}, req.language);
} else if (rarity > 0.3) { // eggs 30% chance

View File

@@ -43,7 +43,7 @@ module.exports = function buyArmoire (user, req = {}, analytics) {
drop = randomVal(eligibleEquipment);
if (user.items.gear.owned[drop.key]) {
throw new NotAuthorized(i18n.t('equipmentAlradyOwned', req.language));
throw new NotAuthorized(i18n.t('equipmentAlreadyOwned', req.language));
}
user.items.gear.owned[drop.key] = true;
@@ -74,14 +74,12 @@ module.exports = function buyArmoire (user, req = {}, analytics) {
message = i18n.t('armoireFood', {
image: `<span class="Pet_Food_${drop.key} pull-left"></span>`,
dropArticle: drop.article,
dropText: drop.text(req.language),
}, req.language);
armoireResp = {
type: 'food',
dropKey: drop.key,
dropArticle: drop.article,
dropText: drop.text(req.language),
dropText: drop.textA(req.language),
};
} else {
let armoireExp = Math.floor(randomVal.trueRandom() * 40 + 10);

View File

@@ -62,7 +62,7 @@ module.exports = function feed (user, req = {}) {
} else {
let messageParams = {
egg: pet.text(req.language),
foodText: food.text(req.language),
foodText: food.textThe(req.language),
};
if (food.target === pet.potion || pet.type === 'premium') {

View File

@@ -243,11 +243,24 @@ module.exports = function scoreTask (options = {}, req = {}) {
if (user.addNotification) user.addNotification('STREAK_ACHIEVEMENT');
}
task.completed = true;
// Save history entry for daily
task.history = task.history || [];
let historyEntry = {
date: Number(new Date()),
value: task.value,
};
task.history.push(historyEntry);
} else if (direction === 'down') {
// Remove a streak achievement if streak was a multiple of 21 and the daily was undone
if (task.streak !== 0 && task.streak % 21 === 0) user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0;
task.streak -= 1;
task.completed = false;
// Delete history entry when daily unchecked
if (task.history || task.history.length > 0) {
task.history.splice(-1, 1);
}
}
}
} else if (task.type === 'todo') {

View File

@@ -248,7 +248,8 @@ function _getMembersForItem (type) {
}
if (req.query.search) {
query['profile.name'] = {$regex: req.query.search};
// Creates a RegExp expression when querying for profile.name
query['profile.name'] = { $regex: new RegExp(req.query.search, 'i') };
}
} else if (type === 'group-invites') {
if (group.type === 'guild') { // eslint-disable-line no-lonely-if

View File

@@ -531,7 +531,7 @@ api.updateTask = {
* {"success":true,"data":{"delta":0.9746999906450404,"_tmp":{},"hp":49.06645205596985,"mp":37.2008917491047,"exp":101.93810026267543,"gp":77.09694176716997,"lvl":19,"class":"rogue","points":0,"str":5,"con":3,"int":3,"per":8,"buffs":{"str":9,"int":9,"per":9,"con":9,"stealth":0,"streaks":false,"snowball":false,"spookySparkles":false,"shinySeed":false,"seafoam":false},"training":{"int":0,"per":0,"str":0,"con":0}},"notifications":[]}
*
* @apiSuccessExample {json} Example result with item drop:
* {"success":true,"data":{"delta":1.0259567046270648,"_tmp":{"quest":{"progressDelta":1.2362778290756147,"collection":1},"drop":{"target":"Zombie","article":"","canDrop":true,"value":1,"key":"RottenMeat","type":"Food","dialog":"You've found Rotten Meat! Feed this to a pet and it may grow into a sturdy steed."}},"hp":50,"mp":66.2390716654227,"exp":143.93810026267545,"gp":135.12889840462591,"lvl":20,"class":"rogue","points":0,"str":6,"con":3,"int":3,"per":8,"buffs":{"str":10,"int":10,"per":10,"con":10,"stealth":0,"streaks":false,"snowball":false,"spookySparkles":false,"shinySeed":false,"seafoam":false},"training":{"int":0,"per":0,"str":0,"con":0}},"notifications":[]}
* {"success":true,"data":{"delta":1.0259567046270648,"_tmp":{"quest":{"progressDelta":1.2362778290756147,"collection":1},"drop":{"target":"Zombie","canDrop":true,"value":1,"key":"RottenMeat","type":"Food","dialog":"You've found Rotten Meat! Feed this to a pet and it may grow into a sturdy steed."}},"hp":50,"mp":66.2390716654227,"exp":143.93810026267545,"gp":135.12889840462591,"lvl":20,"class":"rogue","points":0,"str":6,"con":3,"int":3,"per":8,"buffs":{"str":10,"int":10,"per":10,"con":10,"stealth":0,"streaks":false,"snowball":false,"spookySparkles":false,"shinySeed":false,"seafoam":false},"training":{"int":0,"per":0,"str":0,"con":0}},"notifications":[]}
*
* @apiUse TaskNotFound
*/

View File

@@ -357,14 +357,15 @@ export function cron (options = {}) {
}
}
}
}
// add history entry when task was not completed
task.history.push({
date: Number(new Date()),
value: task.value,
});
task.completed = false;
}
task.completed = false;
setIsDueNextDue(task, user, now);
if (completed || scheduleMisses > 0) {

View File

@@ -1,6 +1,5 @@
import nconf from 'nconf';
import logger from './logger';
import autoinc from 'mongoose-id-autoinc';
import mongoose from 'mongoose';
import Bluebird from 'bluebird';
@@ -12,17 +11,16 @@ mongoose.Promise = Bluebird;
// Do not connect to MongoDB when in maintenance mode
if (MAINTENANCE_MODE !== 'true') {
let mongooseOptions = !IS_PROD ? {} : {
replset: { socketOptions: { keepAlive: 120, connectTimeoutMS: 30000 } },
server: { socketOptions: { keepAlive: 120, connectTimeoutMS: 30000 } },
const mongooseOptions = !IS_PROD ? {} : {
keepAlive: 120,
connectTimeoutMS: 30000,
useMongoClient: true,
};
const NODE_DB_URI = nconf.get('IS_TEST') ? nconf.get('TEST_DB_URI') : nconf.get('NODE_DB_URI');
let db = mongoose.connect(NODE_DB_URI, mongooseOptions, (err) => {
mongoose.connect(NODE_DB_URI, mongooseOptions, (err) => {
if (err) throw err;
logger.info('Connected with Mongoose.');
});
autoinc.init(db);
}

View File

@@ -39,7 +39,8 @@ module.exports = function errorHandler (err, req, res, next) { // eslint-disable
// Handle mongoose validation errors
if (err.name === 'ValidationError') {
responseErr = new BadRequest(err.message); // TODO standard message? translate?
const model = err.message.split(' ')[0];
responseErr = new BadRequest(`${model} validation failed`);
responseErr.errors = map(err.errors, (mongooseErr) => {
return {
message: mongooseErr.message,

View File

@@ -37,7 +37,7 @@ const SESSION_SECRET = nconf.get('SESSION_SECRET');
const TEN_YEARS = 1000 * 60 * 60 * 24 * 365 * 10;
module.exports = function attachMiddlewares (app, server) {
app.set('view engine', 'jade');
app.set('view engine', 'pug');
app.set('views', `${__dirname}/../../views`);
app.use(domainMiddleware(server, mongoose));

View File

@@ -11,7 +11,7 @@ const TOP_LEVEL_CONTROLLERS_PATH = path.join(__dirname, '/../controllers/top-lev
const v3app = express();
// re-set the view options because they are not inherited from the top level app
v3app.set('view engine', 'jade');
v3app.set('view engine', 'pug');
v3app.set('views', `${__dirname}/../../views`);
v3app.use(expressValidator());

View File

@@ -466,9 +466,54 @@ export function chatDefaults (msg, user) {
return message;
}
function setUserStyles (newMessage, user) {
let userStyles = {};
userStyles.items = {gear: {}};
let userCopy = user;
if (user.toObject) userCopy = user.toObject();
if (userCopy.items) {
userStyles.items.gear = {};
userStyles.items.gear.costume = Object.assign({}, userCopy.items.gear.costume);
userStyles.items.gear.equipped = Object.assign({}, userCopy.items.gear.equipped);
userStyles.items.currentMount = userCopy.items.currentMount;
userStyles.items.currentPet = userCopy.items.currentPet;
}
if (userCopy.preferences) {
userStyles.preferences = {};
if (userCopy.preferences.style) userStyles.preferences.style = userCopy.preferences.style;
userStyles.preferences.hair = userCopy.preferences.hair;
userStyles.preferences.skin = userCopy.preferences.skin;
userStyles.preferences.shirt = userCopy.preferences.shirt;
userStyles.preferences.chair = userCopy.preferences.chair;
userStyles.preferences.size = userCopy.preferences.size;
userStyles.preferences.chair = userCopy.preferences.chair;
userStyles.preferences.background = userCopy.preferences.background;
userStyles.preferences.costume = userCopy.preferences.costume;
}
userStyles.stats = {};
if (userCopy.stats && userCopy.stats.buffs) {
userStyles.stats.buffs = {
seafoam: userCopy.stats.buffs.seafoam,
shinySeed: userCopy.stats.buffs.shinySeed,
spookySparkles: userCopy.stats.buffs.spookySparkles,
snowball: userCopy.stats.buffs.snowball,
};
}
newMessage.userStyles = userStyles;
}
schema.methods.sendChat = function sendChat (message, user, metaData) {
let newMessage = chatDefaults(message, user);
if (user) setUserStyles(newMessage, user);
// Optional data stored in the chat message but not returned
// to the users that can be stored for debugging purposes
if (metaData) {
@@ -736,7 +781,7 @@ async function _updateUserWithRetries (userId, updates, numTry = 1, query = {})
return raw;
}).catch((err) => {
if (numTry < MAX_UPDATE_RETRIES) {
return _updateUserWithRetries(userId, updates, ++numTry);
return _updateUserWithRetries(userId, updates, ++numTry, query);
} else {
throw err;
}

View File

@@ -54,6 +54,23 @@ export let TaskSchema = new Schema({
return !validator.isUUID(val);
},
msg: 'Task short names cannot be uuids.',
}, {
validator (alias) {
return new Promise((resolve, reject) => {
Task.findOne({ // eslint-disable-line no-use-before-define
_id: { $ne: this._id },
userId: this.userId,
alias,
}).exec().then((task) => {
let aliasAvailable = !task;
return aliasAvailable ? resolve() : reject();
}).catch(() => {
reject();
});
});
},
msg: 'Task alias already used on another task.',
}],
},
tags: [{
@@ -193,20 +210,6 @@ TaskSchema.methods.scoreChallengeTask = async function scoreChallengeTask (delta
export let Task = mongoose.model('Task', TaskSchema);
Task.schema.path('alias').validate(function valiateAliasNotTaken (alias, respond) {
Task.findOne({
_id: { $ne: this._id },
userId: this.userId,
alias,
}).exec().then((task) => {
let aliasAvailable = !task;
respond(aliasAvailable);
}).catch(() => {
respond(false);
});
}, 'Task alias already used on another task.');
// habits and dailies shared fields
let habitDailySchema = () => {
return {history: Array}; // [{date:Date, value:Number}], // this causes major performance problems