Merge branch 'develop' into release

This commit is contained in:
Sabe Jones 2018-07-19 13:38:00 -05:00
commit 37364b0700
28 changed files with 227 additions and 53 deletions

View file

@ -388,6 +388,23 @@ describe('POST /chat', () => {
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with a max length of 3000 chars', async () => {
const veryLongMessage = `
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.
THIS PART WON'T BE IN THE MESSAGE (over 3000)
`;
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: veryLongMessage});
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
expect(newMessage.message.id).to.exist;
expect(groupMessages[0].id).to.exist;
expect(newMessage.message.text.length).to.eql(3000);
expect(newMessage.message.text).to.not.contain('MESSAGE');
expect(groupMessages[0].text.length).to.eql(3000);
});
it('creates a chat with user styles', async () => {
const mount = 'test-mount';
const pet = 'test-pet';

View file

@ -23,6 +23,17 @@ describe('GET /export/userdata.xml', () => {
]);
// add pinnedItem
await user.get('/user/toggle-pinned-item/marketGear/gear.flat.shield_rogue_5');
// add a private message
let receiver = await generateUser();
user.post('/members/send-private-message', {
message: 'Your first message, hi!',
toUserId: receiver._id,
});
let response = await user.get('/export/userdata.xml');
let {user: res} = await parseStringAsync(response, {explicitArray: false});

View file

@ -62,7 +62,11 @@ module.exports = {
target: DEV_BASE_URL,
changeOrigin: true,
},
'/logout': {
'/logout-server': {
target: DEV_BASE_URL,
changeOrigin: true,
},
'/export': {
target: DEV_BASE_URL,
changeOrigin: true,
},

View file

@ -331,9 +331,33 @@ export default {
];
if (notificationNotFoundMessage.indexOf(errorMessage) !== -1) snackbarTimeout = true;
let errorsToShow = [];
let usernameCheck = false;
let emailCheck = false;
let passwordCheck = false;
// show only the first error for each param
if (errorData.errors) {
for (let e of errorData.errors) {
if (!usernameCheck && e.param === 'username') {
errorsToShow.push(e.message);
usernameCheck = true;
}
if (!emailCheck && e.param === 'email') {
errorsToShow.push(e.message);
emailCheck = true;
}
if (!passwordCheck && e.param === 'password') {
errorsToShow.push(e.message);
passwordCheck = true;
}
}
} else {
errorsToShow.push(errorMessage);
}
// dispatch as one snackbar notification
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: errorMessage,
text: errorsToShow.join(' '),
type: 'error',
timeout: snackbarTimeout,
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -0,0 +1,13 @@
<script>
export default {
components: {},
methods: {
async logout () {
return await this.$store.dispatch('auth:logout');
},
},
created () {
this.logout();
},
};
</script>

View file

@ -402,11 +402,6 @@ export default {
window.location.href = redirectTo;
},
async login () {
if (!this.username) {
alert('Email is required');
return;
}
await this.$store.dispatch('auth:login', {
username: this.username,
// email: this.email,

View file

@ -3,7 +3,7 @@
challenge-modal(v-on:updatedChallenge='updatedChallenge')
leave-challenge-modal(:challengeId='challenge._id')
close-challenge-modal(:members='members', :challengeId='challenge._id')
challenge-member-progress-modal(:memberId='progressMemberId', :challengeId='challenge._id')
challenge-member-progress-modal(:challengeId='challenge._id')
.col-12.col-md-8.standard-page
.row
.col-12.col-md-6
@ -231,7 +231,6 @@ export default {
creatingTask: {},
workingTask: {},
taskFormPurpose: 'create',
progressMemberId: '',
searchTerm: '',
memberResults: [],
};
@ -345,6 +344,7 @@ export default {
this.tasksByType[task.type].splice(index, 1);
},
showMemberModal () {
// @TODO: Change these to options and add a custom event to members modal
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;
@ -374,8 +374,9 @@ export default {
Object.assign(this.challenge, eventData.challenge);
},
openMemberProgressModal (member) {
this.progressMemberId = member._id;
this.$root.$emit('bv::show::modal', 'challenge-member-modal');
this.$root.$emit('habitica:challenge:member-progress', {
progressMemberId: member._id,
});
},
async exportChallengeCsv () {
// let response = await this.$store.dispatch('challenges:exportChallengeCsv', {

View file

@ -1,5 +1,8 @@
<template lang="pug">
b-modal#challenge-member-modal(title="User Progress", size='lg')
.row.award-row
.col-12.text-center
button.btn.btn-primary(v-once, @click='closeChallenge()') {{ $t('awardWinners') }}
.row
task-column.col-6(
v-for="column in columns",
@ -8,12 +11,19 @@
:taskListOverride='tasksByType[column]')
</template>
<style scoped>
.award-row {
padding-top: 1rem;
padding-bottom: 1rem;
}
</style>
<script>
import axios from 'axios';
import Column from '../tasks/column';
export default {
props: ['challengeId', 'memberId'],
props: ['challengeId'],
components: {
TaskColumn: Column,
},
@ -26,8 +36,19 @@ export default {
todo: [],
reward: [],
},
memberId: '',
};
},
mounted () {
this.$root.$on('habitica:challenge:member-progress', (data) => {
if (!data.progressMemberId) return;
this.memberId = data.progressMemberId;
this.$root.$emit('bv::show::modal', 'challenge-member-modal');
});
},
beforeDestroy () {
this.$root.$off('habitica:challenge:member-progress');
},
watch: {
async memberId (id) {
if (!id) return;
@ -45,5 +66,14 @@ export default {
});
},
},
methods: {
async closeChallenge () {
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {
challengeId: this.challengeId,
winnerId: this.memberId,
});
this.$router.push('/challenges/myChallenges');
},
},
};
</script>

View file

@ -100,6 +100,7 @@ export default {
if (!data.message || !data.groupId) return;
this.abuseObject = data.message;
this.groupId = data.groupId;
this.reportComment = '';
this.$root.$emit('bv::show::modal', 'report-flag');
});
},

View file

@ -9,8 +9,10 @@
:class='{"user-entry": newMessage}',
@keydown='updateCarretPosition',
@keyup.ctrl.enter='sendMessageShortcut()',
@paste='disableMessageSendShortcut()'
@paste='disableMessageSendShortcut()',
maxlength='3000'
)
span {{ currentLength }} / 3000
autocomplete(
:text='newMessage',
v-on:select="selectedAutocomplete",
@ -62,6 +64,11 @@
},
};
},
computed: {
currentLength () {
return this.newMessage.length;
},
},
methods: {
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
getCoord (e, text) {

View file

@ -53,6 +53,9 @@ div
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.removeIcon")
span.text {{$t('removeManager2')}}
b-dropdown-item(@click='viewProgress(member)')
span.dropdown-icon-item
span.text {{ $t('viewProgress') }}
.row(v-if='isLoadMoreAvailable')
.col-12.text-center
button.btn.btn-secondary(@click='loadMoreMembers()') {{ $t('loadMore') }}
@ -475,6 +478,11 @@ export default {
groupData.leader = member;
this.$root.$emit('updatedGroup', groupData);
},
viewProgress (member) {
this.$root.$emit('habitica:challenge:member-progress', {
progressMemberId: member._id,
});
},
},
};
</script>

View file

@ -48,7 +48,6 @@
.member-stats {
padding-left: 12px;
padding-right: 24px;
margin-right: 1px;
opacity: 1;
transition: width 0.15s ease-out;
}
@ -148,6 +147,7 @@
right: 100%;
height: calc(100% + 18px);
margin-top: -9px;
margin-right: 1px;
padding-top: 9px;
padding-bottom: 24px;
padding-right: 16px;

View file

@ -50,6 +50,10 @@ transition(name="fade")
.error {
background-color: #f74e52;
border-radius: 60px;
width: 320px !important;
padding: 10px 5px;
margin-left: 0;
color: #fff;
}

View file

@ -1,18 +1,36 @@
<template lang="pug">
.container-fluid
.container-fluid(role="tablist")
.row
.col-12.col-md-6.offset-md-3
h1 {{ $t('frequentlyAskedQuestions') }}
.faq-question(v-for='(heading, index) in headings')
h2.accordion(@click='setActivePage(heading)') {{ $t(`faqQuestion${index}`) }}
div(v-if='pageState[heading]', v-markdown="$t('webFaqAnswer' + index, replacements)")
h1#faq-heading {{ $t('frequentlyAskedQuestions') }}
.faq-question(v-for='(heading, index) in headings', :key="index")
h2(role="tab", v-b-toggle="heading", @click="handleClick($event)", variant="info") {{ $t(`faqQuestion${index}`) }}
b-collapse(:id="heading", :visible="isVisible(heading)", accordion="faq", role="tabpanel")
div.card-body(v-markdown="$t('webFaqAnswer' + index, replacements)")
hr
p(v-markdown="$t('webFaqStillNeedHelp')")
</template>
<style lang='scss' scoped>
.faq-question {
margin-bottom: 1em;
.card-body {
margin-bottom: 1em;
}
.faq-question h2 {
cursor: pointer;
}
.faq-question .card-body {
padding: 0;
}
.static-wrapper .faq-question h2 {
margin: 0 0 16px 0;
}
.faq-question a {
text-decoration: none;
color: #4F2A93;
}
@media only screen and (max-width: 768px) {
@ -25,6 +43,7 @@
<script>
// @TODO: env.EMAILS.TECH_ASSISTANCE_EMAIL
const TECH_ASSISTANCE_EMAIL = 'admin@habitica.com';
import markdownDirective from 'client/directives/markdown';
export default {
@ -48,19 +67,15 @@
'world-boss',
];
let pageState = {};
for (let index in headings) {
let heading = headings[index];
pageState[heading] = false;
}
const hash = window.location.hash.replace('#', '');
return {
pageState,
headings,
replacements: {
techAssistanceEmail: TECH_ASSISTANCE_EMAIL,
wikiTechAssistanceEmail: `mailto:${TECH_ASSISTANCE_EMAIL}`,
},
visible: hash && headings.includes(hash) ? hash : null,
// @TODO webFaqStillNeedHelp: {
// linkStart: '[',
// linkEnd: '](/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)',
@ -69,8 +84,13 @@
};
},
methods: {
setActivePage (page) {
this.pageState[page] = !this.pageState[page];
isVisible (heading) {
return this.visible && this.visible === heading;
},
handleClick (e) {
if (!e) return;
const heading = e.target.nextElementSibling.id;
history.pushState({}, heading, `#${heading}`);
},
},
};

View file

@ -616,11 +616,6 @@
},
// @TODO this is totally duplicate from the registerLogin component
async register () {
if (this.password !== this.passwordConfirm) {
alert('Passwords must match');
return;
}
let groupInvite = '';
if (this.$route.query && this.$route.query.p) {
groupInvite = this.$route.query.p;

View file

@ -81,6 +81,10 @@
min-height: 556px;
}
.sortable-tasks {
word-break: break-word;
}
.sortable-tasks + .reward-items {
margin-top: 16px;
}
@ -446,6 +450,7 @@ export default {
if (this.type !== 'todo') return;
this.$root.$on('habitica::resync-requested', () => {
if (this.activeFilter.label !== 'complete2') return;
this.loadCompletedTodos(true);
});
},

View file

@ -27,6 +27,7 @@ const PrivacyPage = () => import(/* webpackChunkName: "static" */'./components/s
const TermsPage = () => import(/* webpackChunkName: "static" */'./components/static/terms');
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'./components/auth/registerLoginReset');
const Logout = () => import(/* webpackChunkName: "auth" */'./components/auth/logout');
// User Pages
// const StatsPage = () => import(/* webpackChunkName: "user" */'./components/userMenu/stats');
@ -105,6 +106,7 @@ const router = new VueRouter({
routes: [
{ name: 'register', path: '/register', component: RegisterLoginReset, meta: {requiresLogin: false} },
{ name: 'login', path: '/login', component: RegisterLoginReset, meta: {requiresLogin: false} },
{ name: 'logout', path: '/logout', component: Logout },
{ name: 'resetPassword', path: '/reset-password', component: RegisterLoginReset, meta: {requiresLogin: false} },
{ name: 'tasks', path: '/', component: UserTasks },
{

View file

@ -68,5 +68,5 @@ export async function socialAuth (store, params) {
export function logout () {
localStorage.removeItem(LOCALSTORAGE_AUTH_KEY);
localStorage.removeItem(LOCALSTORAGE_SOCIAL_AUTH_KEY);
window.location.href = '/logout';
window.location.href = '/logout-server';
}

View file

@ -132,6 +132,7 @@
"locationRequired": "Location of challenge is required ('Add to')",
"categoiresRequired": "One or more categories must be selected",
"viewProgressOf": "View Progress Of",
"viewProgress": "View Progress",
"selectMember": "Select Member",
"confirmKeepChallengeTasks": "Do you want to keep challenge tasks?",
"selectParticipant": "Select a Participant"

View file

@ -12,12 +12,15 @@ import { removePinnedGearByClass, removePinnedItemsByOwnedGear, addPinnedGearByC
function resetClass (user, req = {}) {
removePinnedGearByClass(user);
let balanceRemoved = 0;
if (user.preferences.disableClasses) {
user.preferences.disableClasses = false;
user.preferences.autoAllocate = false;
} else {
if (user.balance < 0.75) throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
user.balance -= 0.75;
balanceRemoved = 0.75;
}
user.stats.str = 0;
@ -26,19 +29,22 @@ function resetClass (user, req = {}) {
user.stats.int = 0;
user.stats.points = capByLevel(user.stats.lvl);
user.flags.classSelected = false;
return balanceRemoved;
}
module.exports = function changeClass (user, req = {}, analytics) {
let klass = get(req, 'query.class');
let balanceRemoved = 0;
// user.flags.classSelected is set to false after the user paid the 3 gems
if (user.stats.lvl < 10) {
throw new NotAuthorized(i18n.t('lvl10ChangeClass', req.language));
} else if (!klass) {
// if no class is specified, reset points and set user.flags.classSelected to false. User will have paid 3 gems and will be prompted to select class.
resetClass(user, req);
balanceRemoved = resetClass(user, req);
} else if (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer') {
if (user.flags.classSelected) {
resetClass(user, req);
balanceRemoved = resetClass(user, req);
}
user.stats.class = klass;
@ -55,8 +61,8 @@ module.exports = function changeClass (user, req = {}, analytics) {
analytics.track('change class', {
uuid: user._id,
class: klass,
acquireMethod: 'Gems',
gemCost: 3,
acquireMethod: balanceRemoved === 0 ? 'Free' : 'Gems',
gemCost: balanceRemoved / 0.25,
category: 'behavior',
headers: req.headers,
});

View file

@ -101,21 +101,25 @@ api.registerLocal = {
let existingUser = res.locals.user; // If adding local auth to social user
req.checkBody({
email: {
notEmpty: {errorMessage: res.t('missingEmail')},
isEmail: {errorMessage: res.t('notAnEmail')},
},
username: {
notEmpty: {errorMessage: res.t('missingUsername')},
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')},
notEmpty: true,
errorMessage: res.t('missingUsername'),
// TODO use the constants in the error message above
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')},
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameBadCharacters')},
},
email: {
notEmpty: true,
errorMessage: res.t('missingEmail'),
isEmail: {errorMessage: res.t('notAnEmail')},
},
password: {
notEmpty: {errorMessage: res.t('missingPassword')},
notEmpty: true,
errorMessage: res.t('missingPassword'),
equals: {options: [req.body.confirmPassword], errorMessage: res.t('passwordConfirmationMatch')},
},
});
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
@ -476,7 +480,7 @@ api.updateUsername = {
notEmpty: {errorMessage: res.t('missingPassword')},
},
username: {
notEmpty: { errorMessage: res.t('missingUsername') },
notEmpty: {errorMessage: res.t('missingUsername')},
},
});

View file

@ -196,7 +196,6 @@ api.createGroupPlan = {
// @TODO: Change message
if (group.privacy !== 'private') throw new NotAuthorized(res.t('partyMustbePrivate'));
group.memberCount = await User.count({ $or: [{ 'party._id': group._id }, { guilds: group._id }] }).exec();
group.leader = user._id;
user.guilds.push(group._id);

View file

@ -5,7 +5,7 @@ let api = {};
// Internal authentication routes
// Set a new password after having requested a password reset (GET route to input password)
api.resetPasswordSetNewOne = {
api.resetPasswordSetNewOne = {
method: 'GET',
url: '/static/user/auth/local/reset-password-set-new-one',
runCron: false,
@ -24,7 +24,7 @@ api.resetPasswordSetNewOne = {
// Logout the user from the website.
api.logout = {
method: 'GET',
url: '/logout',
url: '/logout-server',
async handler (req, res) {
if (req.logout) req.logout(); // passportjs method
req.session = null;

View file

@ -83,7 +83,7 @@ api.exportUserHistory = {
// Convert user to json and attach tasks divided by type
// at user.tasks[`${taskType}s`] (user.tasks.{dailys/habits/...})
async function _getUserDataForExport (user) {
async function _getUserDataForExport (user, xmlMode = false) {
let userData = user.toJSON();
userData.tasks = {};
@ -98,6 +98,33 @@ async function _getUserDataForExport (user) {
userData.tasks[`${taskType}s`] = tasksPerType;
});
if (xmlMode) {
// object maps cant be parsed
userData.inbox.messages = _(userData.inbox.messages)
.map(m => {
const flags = Object.keys(m.flags);
m.flags = flags;
return m;
})
.value();
// _id gets parsed as an bytearray => which gets casted to a chararray => "weird chars"
userData.unpinnedItems = userData.unpinnedItems.map(i => {
return {
path: i.path,
type: i.type,
};
});
userData.pinnedItems = userData.pinnedItems.map(i => {
return {
path: i.path,
type: i.type,
};
});
}
return userData;
}
@ -137,7 +164,7 @@ api.exportUserDataXml = {
url: '/export/userdata.xml',
middlewares: [authWithSession],
async handler (req, res) {
let userData = await _getUserDataForExport(res.locals.user);
let userData = await _getUserDataForExport(res.locals.user, true);
res.set({
'Content-Type': 'text/xml',

View file

@ -478,7 +478,7 @@ export function chatDefaults (msg, user) {
const message = {
id,
_id: id,
text: msg,
text: msg.substring(0, 3000),
timestamp: Number(new Date()),
likes: {},
flags: {},