Merge branch 'develop' into release

This commit is contained in:
SabreCat
2018-05-24 18:26:21 +00:00
66 changed files with 904 additions and 832 deletions

22
package-lock.json generated
View File

@@ -151,7 +151,7 @@
}, },
"@sinonjs/formatio": { "@sinonjs/formatio": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz",
"integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==",
"dev": true, "dev": true,
"requires": { "requires": {
@@ -3765,7 +3765,7 @@
}, },
"compression": { "compression": {
"version": "1.7.2", "version": "1.7.2",
"resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.2.tgz",
"integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=",
"requires": { "requires": {
"accepts": "1.3.5", "accepts": "1.3.5",
@@ -7544,7 +7544,6 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz",
"integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==",
"optional": true,
"requires": { "requires": {
"nan": "2.6.2", "nan": "2.6.2",
"node-pre-gyp": "0.6.39" "node-pre-gyp": "0.6.39"
@@ -7581,7 +7580,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz",
"integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
"optional": true,
"requires": { "requires": {
"delegates": "1.0.0", "delegates": "1.0.0",
"readable-stream": "2.2.9" "readable-stream": "2.2.9"
@@ -7815,7 +7813,6 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz",
"integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=",
"optional": true,
"requires": { "requires": {
"fstream": "1.0.11", "fstream": "1.0.11",
"inherits": "2.0.3", "inherits": "2.0.3",
@@ -7826,7 +7823,6 @@
"version": "2.7.4", "version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"optional": true,
"requires": { "requires": {
"aproba": "1.1.1", "aproba": "1.1.1",
"console-control-strings": "1.1.0", "console-control-strings": "1.1.0",
@@ -7915,7 +7911,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
"integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
"optional": true,
"requires": { "requires": {
"assert-plus": "0.2.0", "assert-plus": "0.2.0",
"jsprim": "1.4.0", "jsprim": "1.4.0",
@@ -8013,7 +8008,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz",
"integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=",
"optional": true,
"requires": { "requires": {
"assert-plus": "1.0.0", "assert-plus": "1.0.0",
"extsprintf": "1.0.2", "extsprintf": "1.0.2",
@@ -8073,7 +8067,6 @@
"version": "0.6.39", "version": "0.6.39",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz",
"integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==",
"optional": true,
"requires": { "requires": {
"detect-libc": "1.0.2", "detect-libc": "1.0.2",
"hawk": "3.1.3", "hawk": "3.1.3",
@@ -8102,7 +8095,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz",
"integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==", "integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==",
"optional": true,
"requires": { "requires": {
"are-we-there-yet": "1.1.4", "are-we-there-yet": "1.1.4",
"console-control-strings": "1.1.0", "console-control-strings": "1.1.0",
@@ -8223,7 +8215,6 @@
"version": "2.81.0", "version": "2.81.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz",
"integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=",
"optional": true,
"requires": { "requires": {
"aws-sign2": "0.6.0", "aws-sign2": "0.6.0",
"aws4": "1.6.0", "aws4": "1.6.0",
@@ -8365,7 +8356,6 @@
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz",
"integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=",
"optional": true,
"requires": { "requires": {
"debug": "2.6.8", "debug": "2.6.8",
"fstream": "1.0.11", "fstream": "1.0.11",
@@ -8415,14 +8405,12 @@
"uuid": { "uuid": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz",
"integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=", "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE="
"optional": true
}, },
"verror": { "verror": {
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz",
"integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=",
"optional": true,
"requires": { "requires": {
"extsprintf": "1.0.2" "extsprintf": "1.0.2"
} }
@@ -8431,7 +8419,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz",
"integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==",
"optional": true,
"requires": { "requires": {
"string-width": "1.0.2" "string-width": "1.0.2"
} }
@@ -13626,8 +13613,7 @@
"nan": { "nan": {
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz",
"integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=", "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U="
"optional": true
}, },
"nanomatch": { "nanomatch": {
"version": "1.2.9", "version": "1.2.9",

View File

@@ -123,8 +123,8 @@ describe('GET /challenges/:challengeId/members', () => {
}); });
}); });
// @TODO times out too many times (when it takes more than 8s) it('supports using req.query.lastId to get more members', async function () {
xit('supports using req.query.lastId to get more members', async () => { this.timeout(30000); // @TODO: times out after 8 seconds
let group = await generateGroup(user, {type: 'party', name: generateUUID()}); let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group); let challenge = await generateChallenge(user, group);

View File

@@ -81,7 +81,8 @@ describe('GET /groups/:groupId/invites', () => {
}); });
}); });
it('supports using req.query.lastId to get more invites', async () => { it('supports using req.query.lastId to get more invites', async function () {
this.timeout(30000); // @TODO: times out after 8 seconds
let leader = await generateUser({balance: 4}); let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()}); let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});

View File

@@ -142,8 +142,8 @@ describe('GET /groups/:groupId/members', () => {
}); });
}); });
// @TODO times out too many times (when it takes more than 8s) it('supports using req.query.lastId to get more members', async function () {
xit('supports using req.query.lastId to get more members', async () => { this.timeout(30000); // @TODO: times out after 8 seconds
let leader = await generateUser({balance: 4}); let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()}); let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});

View File

@@ -443,8 +443,7 @@ describe('Purchasing a group plan for group', () => {
await api.createSubscription(data); await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec(); const updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3); expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
}); });

View File

@@ -37,6 +37,22 @@ describe('checkout', () => {
payments.createSubscription.restore(); payments.createSubscription.restore();
}); });
it('should error if there is no token', async () => {
await expect(stripePayments.checkout({
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Missing req.body.id',
name: 'BadRequest',
});
});
it('should error if gem amount is too low', async () => { it('should error if gem amount is too low', async () => {
let receivingUser = new User(); let receivingUser = new User();
receivingUser.save(); receivingUser.save();
@@ -64,7 +80,6 @@ describe('checkout', () => {
}); });
}); });
it('should error if user cannot get gems', async () => { it('should error if user cannot get gems', async () => {
gift = undefined; gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);

View File

@@ -1,4 +1,4 @@
import buySpecialSpell from '../../../../website/common/script/ops/buy/buySpecialSpell'; import {BuySpellOperation} from '../../../../website/common/script/ops/buy/buySpell';
import { import {
BadRequest, BadRequest,
NotFound, NotFound,
@@ -15,6 +15,11 @@ describe('shared.ops.buySpecialSpell', () => {
let user; let user;
let analytics = {track () {}}; let analytics = {track () {}};
function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => { beforeEach(() => {
user = generateUser(); user = generateUser();
sinon.stub(analytics, 'track'); sinon.stub(analytics, 'track');

View File

@@ -44,8 +44,6 @@ div
router-view router-view
app-footer app-footer
audio#sound(autoplay, ref="sound") audio#sound(autoplay, ref="sound")
source#oggSource(type="audio/ogg", :src="sound.oggSource")
source#mp3Source(type="audio/mp3", :src="sound.mp3Source")
</template> </template>
<style lang='scss' scoped> <style lang='scss' scoped>
@@ -127,7 +125,7 @@ div
} }
#app-header { #app-header {
margin-top: 96px !important; margin-top: 40px !important;
} }
} }
@@ -220,10 +218,9 @@ export default {
selectedItemToBuy: null, selectedItemToBuy: null,
selectedSpellToBuy: null, selectedSpellToBuy: null,
sound: { audioSource: null,
oggSource: '', audioSuffix: null,
mp3Source: '',
},
loading: true, loading: true,
currentTipNumber: 0, currentTipNumber: 0,
bannerHidden: false, bannerHidden: false,
@@ -259,11 +256,22 @@ export default {
return; return;
} }
let file = `/static/audio/${theme}/${sound}`; let file = `/static/audio/${theme}/${sound}`;
this.sound = {
oggSource: `${file}.ogg`, if (this.audioSuffix === null) {
mp3Source: `${file}.mp3`, this.audioSource = document.createElement('source');
}; if (this.$refs.sound.canPlayType('audio/ogg')) {
this.audioSuffix = '.ogg';
this.audioSource.type = 'audio/ogg';
} else {
this.audioSuffix = '.mp3';
this.audioSource.type = 'audio/mp3';
}
this.audioSource.src = file + this.audioSuffix;
this.$refs.sound.appendChild(this.audioSource);
} else {
this.audioSource.src = file + this.audioSuffix;
}
this.$refs.sound.load(); this.$refs.sound.load();
}); });
@@ -317,7 +325,7 @@ export default {
title: 'Habitica', title: 'Habitica',
text: errorMessage, text: errorMessage,
type: 'error', type: 'error',
timeout: true, timeout: false,
}); });
} }

View File

@@ -433,12 +433,10 @@ export default {
await hello(network).logout(); await hello(network).logout();
} catch (e) {} // eslint-disable-line } catch (e) {} // eslint-disable-line
const url = window.location.href;
let auth = await hello(network).login({ let auth = await hello(network).login({
scope: 'email', scope: 'email',
// explicitly pass the redirect url or it might redirect to /home // explicitly pass the redirect url or it might redirect to /home
redirect_uri: url, // eslint-disable-line camelcase redirect_uri: '', // eslint-disable-line camelcase
}); });
await this.$store.dispatch('auth:socialAuth', { await this.$store.dispatch('auth:socialAuth', {

View File

@@ -30,7 +30,6 @@
:categories="challenge.categories", :categories="challenge.categories",
:owner="isOwner", :owner="isOwner",
:member="isMember", :member="isMember",
v-once
) )
.challenge-description(v-markdown='challenge.summary') .challenge-description(v-markdown='challenge.summary')
.well-wrapper(v-if="fullLayout") .well-wrapper(v-if="fullLayout")

View File

@@ -0,0 +1,222 @@
<template lang="pug">
.row.chat-row
.col-12
h3(v-once) {{ label }}
.row
textarea(:placeholder='placeholder',
v-model='newMessage',
:class='{"user-entry": newMessage}',
@keydown='updateCarretPosition',
@keyup.ctrl.enter='sendMessageShortcut()',
@paste='disableMessageSendShortcut()'
)
autocomplete(
:text='newMessage',
v-on:select="selectedAutocomplete",
:coords='coords',
:chat='group.chat')
.row.chat-actions
.col-6.chat-receive-actions
button.btn.btn-secondary.float-left.fetch(v-once, @click='fetchRecentMessages()') {{ $t('fetchRecentMessages') }}
button.btn.btn-secondary.float-left(v-once, @click='reverseChat()') {{ $t('reverseChat') }}
.col-6.chat-send-actions
button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }}
community-guidelines
slot(
name="additionRow",
)
.row
.hr.col-12
chat-message(:chat.sync='group.chat', :group-id='group._id', :group-name='group.name')
</template>
<script>
import debounce from 'lodash/debounce';
import autocomplete from '../chat/autoComplete';
import communityGuidelines from './communityGuidelines';
import chatMessage from '../chat/chatMessages';
export default {
props: ['label', 'group', 'placeholder'],
components: {
autocomplete,
communityGuidelines,
chatMessage,
},
data () {
return {
newMessage: '',
chat: {
submitDisable: false,
submitTimeout: null,
},
coords: {
TOP: 0,
LEFT: 0,
},
};
},
methods: {
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
getCoord (e, text) {
let carPos = text.selectionEnd;
let div = document.createElement('div');
let span = document.createElement('span');
let copyStyle = getComputedStyle(text);
[].forEach.call(copyStyle, (prop) => {
div.style[prop] = copyStyle[prop];
});
div.style.position = 'absolute';
document.body.appendChild(div);
div.textContent = text.value.substr(0, carPos);
span.textContent = text.value.substr(carPos) || '.';
div.appendChild(span);
this.coords = {
TOP: span.offsetTop,
LEFT: span.offsetLeft,
};
document.body.removeChild(div);
},
updateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
this._updateCarretPosition(eventUpdate);
}, 250),
_updateCarretPosition (eventUpdate) {
let text = eventUpdate.target;
this.getCoord(eventUpdate, text);
},
async sendMessageShortcut () {
// If the user recently pasted in the text field, don't submit
if (!this.chat.submitDisable) {
this.sendMessage();
}
},
async sendMessage () {
let response = await this.$store.dispatch('chat:postChat', {
group: this.group,
message: this.newMessage,
});
this.group.chat.unshift(response.message);
this.newMessage = '';
// @TODO: I would like to not reload everytime we send. Realtime/Firebase?
let chat = await this.$store.dispatch('chat:getChat', {groupId: this.group._id});
this.group.chat = chat;
},
disableMessageSendShortcut () {
// Some users were experiencing accidental sending of messages after pasting
// So, after pasting, disable the shortcut for a second.
this.chat.submitDisable = true;
if (this.chat.submitTimeout) {
// If someone pastes during the disabled period, prevent early re-enable
clearTimeout(this.chat.submitTimeout);
this.chat.submitTimeout = null;
}
this.chat.submitTimeout = window.setTimeout(() => {
this.chat.submitTimeout = null;
this.chat.submitDisable = false;
}, 500);
},
selectedAutocomplete (newText) {
this.newMessage = newText;
},
fetchRecentMessages () {
this.$emit('fetchRecentMessages');
},
reverseChat () {
this.group.chat.reverse();
},
},
beforeRouteUpdate (to, from, next) {
// Reset chat
this.newMessage = '';
this.coords = {
TOP: 0,
LEFT: 0,
};
next();
},
};
</script>
<style scoped lang="scss">
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/variables.scss';
.chat-actions {
margin-top: 1em;
.chat-receive-actions {
padding-left: 0;
button {
margin-bottom: 1em;
&:not(:last-child) {
margin-right: 1em;
}
}
}
.chat-send-actions {
padding-right: 0;
}
}
.chat-row {
position: relative;
textarea {
height: 150px;
width: 100%;
background-color: $white;
border: solid 1px $gray-400;
font-size: 16px;
font-style: italic;
line-height: 1.43;
color: $gray-300;
padding: .5em;
}
.user-entry {
font-style: normal;
color: $black;
}
.hr {
width: 100%;
height: 20px;
border-bottom: 1px solid $gray-500;
text-align: center;
margin: 2em 0;
}
.hr-middle {
font-size: 16px;
font-weight: bold;
font-family: 'Roboto Condensed';
line-height: 1.5;
text-align: center;
color: $gray-200;
background-color: $gray-700;
padding: .2em;
margin-top: .2em;
display: inline-block;
width: 100px;
}
}
</style>

View File

@@ -27,25 +27,16 @@
.svg-icon.gem(v-html="icons.gem") .svg-icon.gem(v-html="icons.gem")
span.number {{group.balance * 4}} span.number {{group.balance * 4}}
div(v-once) {{ $t('guildBank') }} div(v-once) {{ $t('guildBank') }}
.row.chat-row chat(
.col-12 :label="$t('chat')",
h3(v-once) {{ $t('chat') }} :group="group",
.row.new-message-row :placeholder="!isParty ? $t('chatPlaceholder') : $t('partyChatPlaceholder')",
textarea(:placeholder="!isParty ? $t('chatPlaceholder') : $t('partyChatPlaceholder')", v-model='newMessage', @keydown='updateCarretPosition', @keyup.ctrl.enter='sendMessageShortcut()', @paste='disableMessageSendShortcut()') @fetchRecentMessages="fetchRecentMessages()"
autocomplete(:text='newMessage', v-on:select="selectedAutocomplete", :coords='coords', :chat='group.chat') )
.row.chat-actions template(slot="additionRow")
.col-6.chat-receive-actions
button.btn.btn-secondary.float-left.fetch(v-once, @click='fetchRecentMessages()') {{ $t('fetchRecentMessages') }}
button.btn.btn-secondary.float-left(v-once, @click='reverseChat()') {{ $t('reverseChat') }}
.col-6.chat-send-actions
button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }}
community-guidelines
.row(v-if='showNoNotificationsMessage') .row(v-if='showNoNotificationsMessage')
.col-12.no-notifications .col-12.no-notifications
| {{$t('groupNoNotifications')}} | {{$t('groupNoNotifications')}}
.row
.col-12.hr
chat-message(:chat.sync='group.chat', :group-id='group._id', :group-name='group.name')
.col-12.col-sm-4.sidebar .col-12.col-sm-4.sidebar
.row(:class='{"guild-background": !isParty}') .row(:class='{"guild-background": !isParty}')
.col-12 .col-12
@@ -260,7 +251,6 @@
<script> <script>
// @TODO: Break this down into components // @TODO: Break this down into components
import debounce from 'lodash/debounce';
import extend from 'lodash/extend'; import extend from 'lodash/extend';
import groupUtilities from 'client/mixins/groupsUtilities'; import groupUtilities from 'client/mixins/groupsUtilities';
import styleHelper from 'client/mixins/styleHelper'; import styleHelper from 'client/mixins/styleHelper';
@@ -271,13 +261,11 @@ import startQuestModal from './startQuestModal';
import questDetailsModal from './questDetailsModal'; import questDetailsModal from './questDetailsModal';
import groupFormModal from './groupFormModal'; import groupFormModal from './groupFormModal';
import inviteModal from './inviteModal'; import inviteModal from './inviteModal';
import chatMessage from '../chat/chatMessages';
import autocomplete from '../chat/autoComplete';
import groupChallenges from '../challenges/groupChallenges'; import groupChallenges from '../challenges/groupChallenges';
import groupGemsModal from 'client/components/groups/groupGemsModal'; import groupGemsModal from 'client/components/groups/groupGemsModal';
import questSidebarSection from 'client/components/groups/questSidebarSection'; import questSidebarSection from 'client/components/groups/questSidebarSection';
import markdownDirective from 'client/directives/markdown'; import markdownDirective from 'client/directives/markdown';
import communityGuidelines from './communityGuidelines'; import chat from './chat';
import sidebarSection from '../sidebarSection'; import sidebarSection from '../sidebarSection';
import userLink from '../userLink'; import userLink from '../userLink';
@@ -300,16 +288,14 @@ export default {
membersModal, membersModal,
startQuestModal, startQuestModal,
groupFormModal, groupFormModal,
chatMessage,
inviteModal, inviteModal,
groupChallenges, groupChallenges,
autocomplete,
questDetailsModal, questDetailsModal,
groupGemsModal, groupGemsModal,
questSidebarSection, questSidebarSection,
communityGuidelines,
sidebarSection, sidebarSection,
userLink, userLink,
chat,
}, },
directives: { directives: {
markdown: markdownDirective, markdown: markdownDirective,
@@ -337,11 +323,6 @@ export default {
submitDisable: false, submitDisable: false,
submitTimeout: null, submitTimeout: null,
}, },
newMessage: '',
coords: {
TOP: 0,
LEFT: 0,
},
}; };
}, },
computed: { computed: {
@@ -392,13 +373,6 @@ export default {
beforeRouteUpdate (to, from, next) { beforeRouteUpdate (to, from, next) {
this.$set(this, 'searchId', to.params.groupId); this.$set(this, 'searchId', to.params.groupId);
// Reset chat
this.newMessage = '';
this.coords = {
TOP: 0,
LEFT: 0,
};
next(); next();
}, },
watch: { watch: {
@@ -450,40 +424,6 @@ export default {
return this.$store.dispatch('members:getGroupMembers', payload); 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) {
let carPos = text.selectionEnd;
let div = document.createElement('div');
let span = document.createElement('span');
let copyStyle = getComputedStyle(text);
[].forEach.call(copyStyle, (prop) => {
div.style[prop] = copyStyle[prop];
});
div.style.position = 'absolute';
document.body.appendChild(div);
div.textContent = text.value.substr(0, carPos);
span.textContent = text.value.substr(carPos) || '.';
div.appendChild(span);
this.coords = {
TOP: span.offsetTop,
LEFT: span.offsetLeft,
};
document.body.removeChild(div);
},
updateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
this._updateCarretPosition(eventUpdate);
}, 250),
_updateCarretPosition (eventUpdate) {
let text = eventUpdate.target;
this.getCoord(eventUpdate, text);
},
selectedAutocomplete (newText) {
this.newMessage = newText;
},
showMemberModal () { showMemberModal () {
this.$store.state.memberModalOptions.groupId = this.group._id; this.$store.state.memberModalOptions.groupId = this.group._id;
this.$store.state.memberModalOptions.group = this.group; this.$store.state.memberModalOptions.group = this.group;
@@ -492,44 +432,9 @@ export default {
this.$store.state.memberModalOptions.fetchMoreMembers = this.loadMembers; this.$store.state.memberModalOptions.fetchMoreMembers = this.loadMembers;
this.$root.$emit('bv::show::modal', 'members-modal'); this.$root.$emit('bv::show::modal', 'members-modal');
}, },
disableMessageSendShortcut () {
// Some users were experiencing accidental sending of messages after pasting
// So, after pasting, disable the shortcut for a second.
this.chat.submitDisable = true;
if (this.chat.submitTimeout) {
// If someone pastes during the disabled period, prevent early re-enable
clearTimeout(this.chat.submitTimeout);
this.chat.submitTimeout = null;
}
this.chat.submitTimeout = window.setTimeout(() => {
this.chat.submitTimeout = null;
this.chat.submitDisable = false;
}, 500);
},
async sendMessageShortcut () {
// If the user recently pasted in the text field, don't submit
if (!this.chat.submitDisable) {
this.sendMessage();
}
},
async sendMessage () {
if (!this.newMessage) return;
let response = await this.$store.dispatch('chat:postChat', {
group: this.group,
message: this.newMessage,
});
this.group.chat.unshift(response.message);
this.newMessage = '';
},
fetchRecentMessages () { fetchRecentMessages () {
this.fetchGuild(); this.fetchGuild();
}, },
reverseChat () {
this.group.chat.reverse();
},
updateGuild () { updateGuild () {
this.$store.state.editingGroup = this.group; this.$store.state.editingGroup = this.group;
this.$root.$emit('bv::show::modal', 'guild-form'); this.$root.$emit('bv::show::modal', 'guild-form');

View File

@@ -7,26 +7,12 @@
.col-6.title-details .col-6.title-details
h1(v-once) {{ $t('welcomeToTavern') }} h1(v-once) {{ $t('welcomeToTavern') }}
.row.chat-row chat(
.col-12 :label="$t('tavernChat')",
h3(v-once) {{ $t('tavernChat') }} :group="group",
:placeholder="$t('tavernCommunityGuidelinesPlaceholder')",
.row @fetchRecentMessages="fetchRecentMessages()"
textarea(:placeholder="$t('tavernCommunityGuidelinesPlaceholder')", v-model='newMessage', :class='{"user-entry": newMessage}', @keydown='updateCarretPosition', @keyup.ctrl.enter='sendMessageShortcut()', @paste='disableMessageSendShortcut()') )
autocomplete(:text='newMessage', v-on:select="selectedAutocomplete", :coords='coords', :chat='group.chat')
.row.chat-actions
.col-6.chat-receive-actions
button.btn.btn-secondary.float-left.fetch(v-once, @click='fetchRecentMessages()') {{ $t('fetchRecentMessages') }}
button.btn.btn-secondary.float-left(v-once, @click='reverseChat()') {{ $t('reverseChat') }}
.col-6.chat-send-actions
button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }}
community-guidelines
.row
.hr.col-12
chat-message(:chat.sync='group.chat', :group-id='group._id', :group-name='group.name')
.col-12.col-sm-4.sidebar .col-12.col-sm-4.sidebar
.section .section
.grassy-meadow-backdrop .grassy-meadow-backdrop
@@ -179,69 +165,6 @@
@import '~client/assets/scss/colors.scss'; @import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/variables.scss'; @import '~client/assets/scss/variables.scss';
.chat-actions {
margin-top: 1em;
.chat-receive-actions {
padding-left: 0;
button {
margin-bottom: 1em;
&:not(:last-child) {
margin-right: 1em;
}
}
}
.chat-send-actions {
padding-right: 0;
}
}
.chat-row {
position: relative;
textarea {
height: 150px;
width: 100%;
background-color: $white;
border: solid 1px $gray-400;
font-size: 16px;
font-style: italic;
line-height: 1.43;
color: $gray-300;
padding: .5em;
}
.user-entry {
font-style: normal;
color: $black;
}
.hr {
width: 100%;
height: 20px;
border-bottom: 1px solid $gray-500;
text-align: center;
margin: 2em 0;
}
.hr-middle {
font-size: 16px;
font-weight: bold;
font-family: 'Roboto Condensed';
line-height: 1.5;
text-align: center;
color: $gray-200;
background-color: $gray-700;
padding: .2em;
margin-top: .2em;
display: inline-block;
width: 100px;
}
}
h1 { h1 {
color: $purple-200; color: $purple-200;
} }
@@ -505,17 +428,15 @@
</style> </style>
<script> <script>
import debounce from 'lodash/debounce';
import { mapState } from 'client/libs/store'; import { mapState } from 'client/libs/store';
import { goToModForm } from 'client/libs/modform'; import { goToModForm } from 'client/libs/modform';
import { TAVERN_ID } from '../../../common/script/constants'; import { TAVERN_ID } from '../../../common/script/constants';
import chatMessage from '../chat/chatMessages';
import autocomplete from '../chat/autoComplete';
import communityGuidelines from './communityGuidelines';
import worldBossInfoModal from '../world-boss/worldBossInfoModal'; import worldBossInfoModal from '../world-boss/worldBossInfoModal';
import worldBossRageModal from '../world-boss/worldBossRageModal'; import worldBossRageModal from '../world-boss/worldBossRageModal';
import sidebarSection from '../sidebarSection'; import sidebarSection from '../sidebarSection';
import chat from './chat';
import challengeIcon from 'assets/svg/challenge.svg'; import challengeIcon from 'assets/svg/challenge.svg';
import chevronIcon from 'assets/svg/chevron-red.svg'; import chevronIcon from 'assets/svg/chevron-red.svg';
@@ -538,15 +459,14 @@ import tierNPC from 'assets/svg/tier-npc.svg';
import tierStaff from 'assets/svg/tier-staff.svg'; import tierStaff from 'assets/svg/tier-staff.svg';
import quests from 'common/script/content/quests'; import quests from 'common/script/content/quests';
import staffList from '../../libs/staffList';
export default { export default {
components: { components: {
chatMessage,
autocomplete,
communityGuidelines,
worldBossInfoModal, worldBossInfoModal,
worldBossRageModal, worldBossRageModal,
sidebarSection, sidebarSection,
chat,
}, },
data () { data () {
return { return {
@@ -571,118 +491,13 @@ export default {
tierNPC, tierNPC,
tierStaff, tierStaff,
}), }),
chat: {
submitDisable: false,
submitTimeout: null,
},
group: { group: {
chat: [], chat: [],
}, },
sections: { sections: {
worldBoss: true, worldBoss: true,
}, },
staff: [ staff: staffList,
{
name: 'beffymaroo',
type: 'Staff',
uuid: '9fe7183a-4b79-4c15-9629-a1aee3873390',
},
// {
// name: 'lefnire',
// type: 'Staff',
// uuid: '00000000-0000-4000-9000-000000000000',
// },
{
name: 'Lemoness',
type: 'Staff',
uuid: '7bde7864-ebc5-4ee2-a4b7-1070d464cdb0',
},
{
name: 'paglias',
type: 'Staff',
uuid: 'ed4c688c-6652-4a92-9d03-a5a79844174a',
},
{
name: 'redphoenix',
type: 'Staff',
uuid: 'cb46ad54-8c78-4dbc-a8ed-4e3185b2b3ff',
},
{
name: 'SabreCat',
type: 'Staff',
uuid: '7f14ed62-5408-4e1b-be83-ada62d504931',
},
{
name: 'TheHollidayInn',
type: 'Staff',
uuid: '206039c6-24e4-4b9f-8a31-61cbb9aa3f66',
},
{
name: 'viirus',
type: 'Staff',
uuid: 'a327d7e0-1c2e-41be-9193-7b30b484413f',
},
{
name: 'It\'s Bailey',
type: 'Moderator',
uuid: '9da65443-ed43-4c21-804f-d260c1361596',
},
{
name: 'Alys',
type: 'Moderator',
uuid: 'd904bd62-da08-416b-a816-ba797c9ee265',
},
{
name: 'Blade',
type: 'Moderator',
uuid: '75f270e8-c5db-4722-a5e6-a83f1b23f76b',
},
{
name: 'Breadstrings',
type: 'Moderator',
uuid: '3b675c0e-d7a6-440c-8687-bc67cd0bf4e9',
},
{
name: 'Cantras',
type: 'Moderator',
uuid: '28771972-ca6d-4c03-8261-e1734aa7d21d',
},
// {
// name: 'Daniel the Bard',
// type: 'Moderator',
// uuid: '1f7c4a74-03a3-4b2c-b015-112d0acbd593',
// },
{
name: 'deilann 5.0.5b',
type: 'Moderator',
uuid: 'e7b5d1e2-3b6e-4192-b867-8bafdb03eeec',
},
{
name: 'Dewines',
type: 'Moderator',
uuid: '262a7afb-6b57-4d81-88e0-80d2e9f6cbdc',
},
{
name: 'Fox_town',
type: 'Moderator',
uuid: 'a05f0152-d66b-4ef1-93ac-4adb195d0031',
},
{
name: 'Megan',
type: 'Moderator',
uuid: '73e5125c-2c87-4004-8ccd-972aeac4f17a',
},
{
name: 'shanaqui',
type: 'Moderator',
uuid: 'bb089388-28ae-4e42-a8fa-f0c2bfb6f779',
},
],
newMessage: '',
coords: {
TOP: 0,
LEFT: 0,
},
}; };
}, },
computed: { computed: {
@@ -699,81 +514,10 @@ export default {
modForm () { modForm () {
goToModForm(this.user); goToModForm(this.user);
}, },
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
getCoord (e, text) {
let carPos = text.selectionEnd;
let div = document.createElement('div');
let span = document.createElement('span');
let copyStyle = getComputedStyle(text);
[].forEach.call(copyStyle, (prop) => {
div.style[prop] = copyStyle[prop];
});
div.style.position = 'absolute';
document.body.appendChild(div);
div.textContent = text.value.substr(0, carPos);
span.textContent = text.value.substr(carPos) || '.';
div.appendChild(span);
this.coords = {
TOP: span.offsetTop,
LEFT: span.offsetLeft,
};
document.body.removeChild(div);
},
updateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
this._updateCarretPosition(eventUpdate);
}, 250),
_updateCarretPosition (eventUpdate) {
let text = eventUpdate.target;
this.getCoord(eventUpdate, text);
},
selectedAutocomplete (newText) {
this.newMessage = newText;
},
toggleSleep () { toggleSleep () {
this.$store.dispatch('user:sleep'); this.$store.dispatch('user:sleep');
}, },
disableMessageSendShortcut () {
// Some users were experiencing accidental sending of messages after pasting
// So, after pasting, disable the shortcut for a second.
this.chat.submitDisable = true;
if (this.chat.submitTimeout) {
// If someone pastes during the disabled period, prevent early re-enable
clearTimeout(this.chat.submitTimeout);
this.chat.submitTimeout = null;
}
this.chat.submitTimeout = window.setTimeout(() => {
this.chat.submitTimeout = null;
this.chat.submitDisable = false;
}, 500);
},
async sendMessageShortcut () {
// If the user recently pasted in the text field, don't submit
if (!this.chat.submitDisable) {
this.sendMessage();
}
},
async sendMessage () {
let response = await this.$store.dispatch('chat:postChat', {
group: this.group,
message: this.newMessage,
});
this.group.chat.unshift(response.message);
this.newMessage = '';
// @TODO: I would like to not reload everytime we send. Realtime/Firebase?
let chat = await this.$store.dispatch('chat:getChat', {groupId: this.group._id});
this.group.chat = chat;
},
async fetchRecentMessages () {
this.group = await this.$store.dispatch('guilds:getGroup', {groupId: TAVERN_ID});
},
reverseChat () {
this.group.chat.reverse();
},
pendingDamage () { pendingDamage () {
if (!this.user.party.quest.progress.up) return 0; if (!this.user.party.quest.progress.up) return 0;
return this.$options.filters.floor(this.user.party.quest.progress.up, 10); return this.$options.filters.floor(this.user.party.quest.progress.up, 10);
@@ -806,6 +550,10 @@ export default {
startingPage: 'profile', startingPage: 'profile',
}); });
}, },
async fetchRecentMessages () {
this.group = await this.$store.dispatch('guilds:getGroup', {groupId: TAVERN_ID});
},
}, },
}; };
</script> </script>

View File

@@ -55,7 +55,6 @@ div
} }
#app-header { #app-header {
margin-top: 56px;
padding-left: 24px; padding-left: 24px;
padding-top: 9px; padding-top: 9px;
padding-bottom: 8px; padding-bottom: 8px;

View File

@@ -3,60 +3,60 @@ div
inbox-modal inbox-modal
creator-intro creator-intro
profile profile
b-navbar.navbar.navbar-inverse.fixed-top.navbar-expand-lg(type="dark", :class="navbarZIndexClass") b-navbar.topbar.navbar-inverse.static-top.navbar-expand-lg(type="dark", :class="navbarZIndexClass")
.navbar-header b-navbar-brand.brand
.logo.svg-icon.d-none.d-xl-block(v-html="icons.logo") .logo.svg-icon.d-none.d-xl-block(v-html="icons.logo")
.svg-icon.gryphon.d-md-block.d-none.d-xl-none .svg-icon.gryphon.d-xs-block.d-xl-none
.svg-icon.gryphon.d-sm-block.d-lg-none.d-md-none b-nav-toggle(target='menu_collapse').menu-toggle
b-nav-toggle(target='nav_collapse') .quick-menu.mobile-only.form-inline
b-collapse#nav_collapse.collapse.navbar-collapse.justify-content-between.flex-wrap(is-nav) a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
.ul.navbar-nav .top-menu-icon.svg-icon(v-html="icons.sync")
router-link.nav-item(tag="li", :to="{name: 'tasks'}", exact) notification-menu.item-with-icon
a.nav-link(v-once) {{ $t('tasks') }} user-dropdown.item-with-icon
router-link.nav-item.dropdown(tag="li", :to="{name: 'items'}", :class="{'active': $route.path.startsWith('/inventory')}") b-collapse#menu_collapse.collapse.navbar-collapse
a.nav-link(v-once) {{ $t('inventory') }} b-navbar-nav.menu-list
.dropdown-menu b-nav-item.topbar-item(tag="li", :to="{name: 'tasks'}", exact) {{ $t('tasks') }}
router-link.dropdown-item(:to="{name: 'items'}", exact) {{ $t('items') }} li.topbar-item(:class="{'active': $route.path.startsWith('/inventory')}")
router-link.dropdown-item(:to="{name: 'equipment'}") {{ $t('equipment') }} router-link.nav-link(:to="{name: 'items'}") {{ $t('inventory') }}
router-link.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }} .topbar-dropdown
router-link.nav-item.dropdown(tag="li", :to="{name: 'market'}", :class="{'active': $route.path.startsWith('/shop')}") router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'items'}", exact) {{ $t('items') }}
a.nav-link(v-once) {{ $t('shops') }} router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'equipment'}") {{ $t('equipment') }}
.dropdown-menu router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }}
router-link.dropdown-item(:to="{name: 'market'}", exact) {{ $t('market') }} li.topbar-item(:class="{'active': $route.path.startsWith('/shop')}")
router-link.dropdown-item(:to="{name: 'quests'}") {{ $t('quests') }} router-link.nav-link(:to="{name: 'market'}") {{ $t('shops') }}
router-link.dropdown-item(:to="{name: 'seasonal'}") {{ $t('titleSeasonalShop') }} .topbar-dropdown
router-link.dropdown-item(:to="{name: 'time'}") {{ $t('titleTimeTravelers') }} router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'market'}", exact) {{ $t('market') }}
router-link.nav-item(tag="li", :to="{name: 'party'}", v-if='this.user.party._id') router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'quests'}") {{ $t('quests') }}
a.nav-link(v-once) {{ $t('party') }} router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'seasonal'}") {{ $t('titleSeasonalShop') }}
.nav-item(@click='openPartyModal()', v-if='!this.user.party._id') router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'time'}") {{ $t('titleTimeTravelers') }}
a.nav-link(v-once) {{ $t('party') }} b-nav-item.topbar-item(tag="li", :to="{name: 'party'}", v-if='this.user.party._id') {{ $t('party') }}
router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/guilds')}") b-nav-item.topbar-item(@click='openPartyModal()', v-if='!this.user.party._id') {{ $t('party') }}
a.nav-link(v-once) {{ $t('guilds') }} li.topbar-item(:class="{'active': $route.path.startsWith('/guilds')}")
.dropdown-menu router-link.nav-link(:to="{name: 'tavern'}") {{ $t('guilds') }}
router-link.dropdown-item(:to="{name: 'tavern'}") {{ $t('tavern') }} .topbar-dropdown
router-link.dropdown-item(:to="{name: 'myGuilds'}") {{ $t('myGuilds') }} router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'tavern'}") {{ $t('tavern') }}
router-link.dropdown-item(:to="{name: 'guildsDiscovery'}") {{ $t('guildsDiscovery') }} router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'myGuilds'}") {{ $t('myGuilds') }}
router-link.nav-item.dropdown(tag="li", :to="{name: 'groupPlan'}", :class="{'active': $route.path.startsWith('/group-plans')}") router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'guildsDiscovery'}") {{ $t('guildsDiscovery') }}
a.nav-link(v-once) {{ $t('group') }} li.topbar-item(:class="{'active': $route.path.startsWith('/group-plans')}")
.dropdown-menu router-link.nav-link(:to="{name: 'groupPlan'}") {{ $t('group') }}
router-link.dropdown-item(v-for='group in groupPlans', :key='group._id', :to="{name: 'groupPlanDetailTaskInformation', params: {groupId: group._id}}") {{ group.name }} .topbar-dropdown
router-link.nav-item.dropdown(tag="li", :to="{name: 'myChallenges'}", :class="{'active': $route.path.startsWith('/challenges')}") router-link.topbar-dropdown-item.dropdown-item(v-for='group in groupPlans', :key='group._id', :to="{name: 'groupPlanDetailTaskInformation', params: {groupId: group._id}}") {{ group.name }}
a.nav-link(v-once) {{ $t('challenges') }} li.topbar-item(:class="{'active': $route.path.startsWith('/challenges')}")
.dropdown-menu router-link.nav-link(:to="{name: 'myChallenges'}") {{ $t('challenges') }}
router-link.dropdown-item(:to="{name: 'myChallenges'}") {{ $t('myChallenges') }} .topbar-dropdown
router-link.dropdown-item(:to="{name: 'findChallenges'}") {{ $t('findChallenges') }} router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'myChallenges'}") {{ $t('myChallenges') }}
router-link.nav-item.dropdown(tag="li", :class="{'active': $route.path.startsWith('/help')}", :to="{name: 'faq'}") router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'findChallenges'}") {{ $t('findChallenges') }}
a.nav-link(v-once) {{ $t('help') }} li.topbar-item(:class="{'active': $route.path.startsWith('/help')}")
.dropdown-menu router-link.nav-link(:to="{name: 'faq'}") {{ $t('help') }}
router-link.dropdown-item(:to="{name: 'faq'}") {{ $t('faq') }} .topbar-dropdown
router-link.dropdown-item(:to="{name: 'overview'}") {{ $t('overview') }} router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'faq'}") {{ $t('faq') }}
router-link.dropdown-item(to="/groups/guild/a29da26b-37de-4a71-b0c6-48e72a900dac") {{ $t('reportBug') }} router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'overview'}") {{ $t('overview') }}
router-link.dropdown-item(to="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a") {{ $t('askAQuestion') }} router-link.topbar-dropdown-item.dropdown-item(to="/groups/guild/a29da26b-37de-4a71-b0c6-48e72a900dac") {{ $t('reportBug') }}
router-link.topbar-dropdown-item.dropdown-item(to="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a") {{ $t('askAQuestion') }}
a.dropdown-item(href="https://trello.com/c/odmhIqyW/440-read-first-table-of-contents", target='_blank') {{ $t('requestAF') }} a.dropdown-item(href="https://trello.com/c/odmhIqyW/440-read-first-table-of-contents", target='_blank') {{ $t('requestAF') }}
a.dropdown-item(href="http://habitica.wikia.com/wiki/Contributing_to_Habitica", target='_blank') {{ $t('contributing') }} a.dropdown-item(href="http://habitica.wikia.com/wiki/Contributing_to_Habitica", target='_blank') {{ $t('contributing') }}
a.dropdown-item(href="http://habitica.wikia.com/wiki/Habitica_Wiki", target='_blank') {{ $t('wiki') }} a.dropdown-item(href="http://habitica.wikia.com/wiki/Habitica_Wiki", target='_blank') {{ $t('wiki') }}
a.dropdown-item(@click='modForm()') {{ $t('contactForm') }} .currency-tray.form-inline
.user-menu.d-flex.align-items-center
.item-with-icon(v-if="userHourglasses > 0") .item-with-icon(v-if="userHourglasses > 0")
.top-menu-icon.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')") .top-menu-icon.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
span {{ userHourglasses }} span {{ userHourglasses }}
@@ -66,6 +66,7 @@ div
.item-with-icon.gold .item-with-icon.gold
.top-menu-icon.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')") .top-menu-icon.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
span {{Math.floor(user.stats.gp * 100) / 100}} span {{Math.floor(user.stats.gp * 100) / 100}}
.form-inline.desktop-only
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')") a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
.top-menu-icon.svg-icon(v-html="icons.sync") .top-menu-icon.svg-icon(v-html="icons.sync")
notification-menu.item-with-icon notification-menu.item-with-icon
@@ -76,16 +77,6 @@ div
@import '~client/assets/scss/colors.scss'; @import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/utils.scss'; @import '~client/assets/scss/utils.scss';
@media only screen and (max-width: 1305px) {
.nav-link {
padding: .8em 1em !important;
}
.navbar-header {
margin-right: 5px !important;
}
}
@media only screen and (max-width: 1200px) { @media only screen and (max-width: 1200px) {
.gryphon { .gryphon {
background-image: url('~assets/images/melior@3x.png'); background-image: url('~assets/images/melior@3x.png');
@@ -94,57 +85,107 @@ div
background-size: cover; background-size: cover;
color: $white; color: $white;
margin: 0 auto; margin: 0 auto;
} }
.svg-icon.gryphon.d-sm-block { .topbar-item {
position: absolute;
left: calc(50% - 30px);
top: 1em;
}
.nav-item .nav-link {
font-size: 14px !important; font-size: 14px !important;
padding: 16px 12px !important;
} }
} }
@media only screen and (max-width: 990px) { @media only screen and (min-width: 992px) {
#nav_collapse { .mobile-only {
margin-top: 0.6em; display: none !important;
flex-direction: row !important;
max-height: 650px;
overflow: auto;
} }
.navbar-nav { .topbar {
width: 100%; max-height: 56px;
background: $purple-100;
}
.user-menu { .currency-tray {
flex-direction: column !important; margin-left: auto;
align-items: left !important; }
background: $purple-100;
width: 100%;
.item-with-icon { .topbar-item {
width: 100%; padding-top: 5px;
padding-bottom: 1em; height: 56px;
&.active:not(:hover) {
box-shadow: 0px -4px 0px $purple-300 inset;
}
}
.topbar-dropdown {
position: absolute;
} }
} }
} }
#nav_collapse { @media only screen and (max-width: 992px) {
display: flex; .brand {
margin: 0;
}
.gryphon {
position: absolute;
left: calc(50% - 30px);
top: 10px;
}
#menu_collapse {
margin: 0.6em -16px -8px;
overflow: auto;
flex-direction: column;
background-color: $purple-100;
.menu-list {
width: 100%;
order: 1;
text-align: center;
.topbar-dropdown-item {
background: #432874;
border-bottom: #6133b4 solid 1px;
}
.topbar-item {
&.active {
background: #6133b4;
}
background: #4f2a93;
border-bottom: #6133b4 solid 1px;
}
}
}
.currency-tray {
justify-content: center;
min-height: 40px;
background: #271b3d;
width: 100%;
}
.desktop-only {
display: none !important;
}
} }
nav.navbar { .menu-toggle {
background: $purple-100 url(~assets/svg/for-css/bits.svg) right no-repeat; border: none;
padding-left: 25px; }
padding-right: 12.5px;
height: 56px; #menu_collapse {
display: flex;
justify-content: space-between;
}
.topbar {
background: $purple-100 url(~assets/svg/for-css/bits.svg) right top no-repeat;
min-height: 56px;
box-shadow: 0 1px 2px 0 rgba($black, 0.24); box-shadow: 0 1px 2px 0 rgba($black, 0.24);
a {
color: white !important;
}
} }
.navbar-z-index { .navbar-z-index {
@@ -154,76 +195,75 @@ div
&-modal { &-modal {
z-index: 1035; z-index: 1035;
z-index: 1042; // To stay above snakbar notifications and modals
} }
} }
.navbar-header { .logo {
margin-right: 48px; padding-left: 10px;
width: 128px;
.logo { height: 28px;
width: 128px;
height: 28px;
}
} }
.nav-item { .quick-menu {
.nav-link { display: flex;
font-size: 16px; margin-left: auto;
color: $white !important; }
font-weight: bold;
line-height: 1.5; .currency-tray {
padding: 16px 20px; display: flex;
transition: none; }
.topbar-item {
font-size: 16px;
color: $white !important;
font-weight: bold;
transition: none;
.topbar-dropdown {
display: none; // Display is set to block on hover.
}
>a {
padding: .8em 1em !important;
} }
&:hover { &:hover {
.nav-link { color: $white !important;
color: $white !important; background: $purple-200;
.topbar-dropdown {
display: block; // Open drop-down on hover.
margin-top: 0; // Remove gap between navbar and drop-down.
background: $purple-200; background: $purple-200;
} border-radius: 0px;
} border: none;
box-shadow: none;
padding: 0px;
&.active:not(:hover) { border-bottom-right-radius: 5px;
.nav-link { border-bottom-left-radius: 5px;
box-shadow: 0px -4px 0px $purple-300 inset;
}
}
}
// Make the dropdown menu open on hover .topbar-dropdown-item {
.dropdown:hover .dropdown-menu { font-size: 16px;
display: block; box-shadow: none;
margin-top: 0; // remove the gap so it doesn't close color: $white;
} border: none;
line-height: 1.5;
display: list-item;
.dropdown-menu { &.active {
background: $purple-200; background: $purple-300;
border-radius: 0px; }
border: none;
box-shadow: none;
padding: 0px;
border-bottom-right-radius: 5px; &:hover {
border-bottom-left-radius: 5px; background: $purple-300;
.dropdown-item { &:last-child {
font-size: 16px; border-bottom-right-radius: 5px;
box-shadow: none; border-bottom-left-radius: 5px;
color: $white; }
border: none; }
line-height: 1.5;
&.active {
background: $purple-300;
}
&:hover {
background: $purple-300;
color: $white;
&:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
} }
} }
} }
@@ -380,6 +420,7 @@ export default {
this.$root.$emit('bv::show::modal', 'buy-gems', {alreadyTracked: true}); this.$root.$emit('bv::show::modal', 'buy-gems', {alreadyTracked: true});
}, },
}, },
}; };
</script> </script>

View File

@@ -935,10 +935,22 @@
} }
}, },
async feedAction (petKey, foodKey) { async feedAction (petKey, foodKey) {
const result = await this.$store.dispatch('common:feed', {pet: petKey, food: foodKey}); try {
if (result.message) this.text(result.message); const result = await this.$store.dispatch('common:feed', {pet: petKey, food: foodKey});
if (this.user.preferences.suppressModals.raisePet) return;
if (this.user.items.pets[petKey] === -1) this.$root.$emit('habitica::mount-raised', petKey); if (result.message) this.text(result.message);
if (this.user.preferences.suppressModals.raisePet) return;
if (this.user.items.pets[petKey] === -1) this.$root.$emit('habitica::mount-raised', petKey);
} catch (e) {
const errorMessage = e.message || e;
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: errorMessage,
type: 'error',
timeout: true,
});
}
}, },
closeHatchPetDialog () { closeHatchPetDialog () {
this.$root.$emit('bv::hide::modal', 'hatching-modal'); this.$root.$emit('bv::hide::modal', 'hatching-modal');

View File

@@ -345,7 +345,7 @@ export default {
const newLang = e.target.value; const newLang = e.target.value;
this.user.preferences.language = newLang; this.user.preferences.language = newLang;
await this.set('language'); await this.set('language');
window.location.href = '/'; setTimeout(() => window.location.reload(true));
}, },
async changeUser (attribute, updates) { async changeUser (attribute, updates) {
await axios.put(`/api/v3/user/auth/update-${attribute}`, updates); await axios.put(`/api/v3/user/auth/update-${attribute}`, updates);

View File

@@ -130,7 +130,6 @@ export default {
}; };
}, },
created () { created () {
// @TODO the notifications always close even if timeout is false
let timeout = this.notification.hasOwnProperty('timeout') ? this.notification.timeout : true; let timeout = this.notification.hasOwnProperty('timeout') ? this.notification.timeout : true;
if (timeout) { if (timeout) {
let delay = this.notification.delay || 1500; let delay = this.notification.delay || 1500;

View File

@@ -7,11 +7,11 @@
#intro-signup.purple-1 #intro-signup.purple-1
.container .container
.row .row
.col-12.col-sm-6.col-md-6.col-lg-6 .col-12.col-md-6.col-lg-6
img(src='~assets/images/home/home-main@3x.png', width='357px') img(src='~assets/images/home/home-main@3x.png', width='357px')
h1 {{$t('motivateYourself')}} h1 {{$t('motivateYourself')}}
p.section-main {{$t('timeToGetThingsDone', {userCountInMillions})}} p.section-main {{$t('timeToGetThingsDone', {userCountInMillions})}}
.col-12.col-sm-6.col-md-6.col-lg-6 .col-12.col-md-6.col-lg-6
h3.text-center {{$t('singUpForFree')}} h3.text-center {{$t('singUpForFree')}}
div.text-center div.text-center
button.social-button(@click='socialAuth("facebook")') button.social-button(@click='socialAuth("facebook")')
@@ -42,15 +42,15 @@
h2 {{$t('gamifyYourLife')}} h2 {{$t('gamifyYourLife')}}
p.section-main {{$t('aboutHabitica')}} p.section-main {{$t('aboutHabitica')}}
.row .row
.col-12.col-sm-4 .col-12.col-md-4
img.track-habits(src='~assets/images/home/track-habits@3x.png', width='354px', height='228px') img.track-habits(src='~assets/images/home/track-habits@3x.png', width='354px', height='228px')
strong {{$t('trackYourGoals')}} strong {{$t('trackYourGoals')}}
p {{$t('trackYourGoalsDesc')}} p {{$t('trackYourGoalsDesc')}}
.col-12.col-sm-4 .col-12.col-md-4
img(src='~assets/images/home/earn-rewards@3x.png', width='316px', height='244px') img(src='~assets/images/home/earn-rewards@3x.png', width='316px', height='244px')
strong {{$t('earnRewards')}} strong {{$t('earnRewards')}}
p {{$t('earnRewardsDesc')}} p {{$t('earnRewardsDesc')}}
.col-12.col-sm-4 .col-12.col-md-4
img(src='~assets/images/home/battle-monsters@3x.png', width='303px', height='244px') img(src='~assets/images/home/battle-monsters@3x.png', width='303px', height='244px')
strong {{$t('battleMonsters')}} strong {{$t('battleMonsters')}}
p {{$t('battleMonstersDesc')}} p {{$t('battleMonstersDesc')}}
@@ -83,9 +83,9 @@
#level-up-anywhere.purple-3 #level-up-anywhere.purple-3
.container .container
.row .row
.col-12.col-sm-6.col-md-6.col-lg-6 .col-12.col-md-6.col-lg-6
.iphones .iphones
.col-12.col-sm-6.col-md-6.col-lg-6.text-column .col-12.col-md-6.col-lg-6.text-column
h2 {{ $t('levelUpAnywhere') }} h2 {{ $t('levelUpAnywhere') }}
p {{ $t('levelUpAnywhereDesc') }} p {{ $t('levelUpAnywhereDesc') }}
a.app.svg-icon(v-html='icons.googlePlay', href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica', target='_blank') a.app.svg-icon(v-html='icons.googlePlay', href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica', target='_blank')
@@ -345,6 +345,9 @@
text-align: center; text-align: center;
img { img {
max-width: 100%;
display: block;
margin: 0 auto;
margin-top: 1em; margin-top: 1em;
margin-bottom: 1.5em; margin-bottom: 1.5em;
} }
@@ -387,6 +390,8 @@
.iphones { .iphones {
width: 436px; width: 436px;
height: 520px; height: 520px;
max-width: 100%;
background-repeat: no-repeat;
background-size: 100%; background-size: 100%;
background-image: url('~assets/images/home/mobile-preview@3x.png'); background-image: url('~assets/images/home/mobile-preview@3x.png');
} }
@@ -658,13 +663,10 @@
await hello(network).logout(); await hello(network).logout();
} catch (e) {} // eslint-disable-line } catch (e) {} // eslint-disable-line
const auth = await hello(network).login({
const url = window.location.href;
let auth = await hello(network).login({
scope: 'email', scope: 'email',
// explicitly pass the redirect url or it might redirect to /home // explicitly pass the redirect url or it might redirect to /home
redirect_uri: url, // eslint-disable-line camelcase redirect_uri: '', // eslint-disable-line camelcase
}); });
await this.$store.dispatch('auth:socialAuth', { await this.$store.dispatch('auth:socialAuth', {

View File

@@ -24,7 +24,7 @@
@focus="quickAddFocused = true", @blur="quickAddFocused = false", @focus="quickAddFocused = true", @blur="quickAddFocused = false",
) )
transition(name="quick-add-tip-slide") transition(name="quick-add-tip-slide")
.quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip')") .quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip', {taskType: $t(typeLabel)})")
clear-completed-todos(v-if="activeFilter.label === 'complete2' && isUser === true") clear-completed-todos(v-if="activeFilter.label === 'complete2' && isUser === true")
.column-background( .column-background(
v-if="isUser === true", v-if="isUser === true",
@@ -370,7 +370,7 @@ export default {
let rewards = inAppRewards(this.user); let rewards = inAppRewards(this.user);
// Add season rewards if user is affected // Add season rewards if user is affected
// @TODO: Add buff coniditional // @TODO: Add buff conditional
const seasonalSkills = { const seasonalSkills = {
snowball: 'salt', snowball: 'salt',
spookySparkles: 'opaquePotion', spookySparkles: 'opaquePotion',

View File

@@ -1,44 +1,57 @@
<template lang="pug"> <template lang="pug">
div.header-tabs .header-tabs
.drawer-tab-container ul.drawer-tab-container
.drawer-tab(v-for="(tab, index) in tabs") li.drawer-tab(v-for="(tab, index) in tabs")
a.drawer-tab-text( a.drawer-tab-text(
@click="changeTab(index)", @click="changeTab(index)",
:class="{'drawer-tab-text-active': selectedTabPosition === index}", :class="{'drawer-tab-text-active': selectedTabPosition === index}",
:title="tab.label" :title="tab.label"
) {{ tab.label }} ) {{ tab.label }}
span.right-item aside.help-item
slot(name="right-item") slot(name="right-item")
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.drawer-tab-text { .drawer-tab-text {
overflow-x: hidden;
display: block; display: block;
overflow-x: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.drawer-tab { .drawer-tab {
white-space: nowrap;
overflow-x: hidden;
flex: inherit; flex: inherit;
overflow-x: hidden;
white-space: nowrap;
} }
.drawer-tab-container { .drawer-tab-container {
max-width: 50%; grid-column-start: 2;
margin: 0 auto; grid-column-end: 3;
justify-self: center;
margin: 0;
padding: 0;
} }
.right-item { .help-item {
position: absolute; grid-column-start: 3;
position: relative;
right: -11px; right: -11px;
text-align: right;
top: -2px; top: -2px;
} }
.header-tabs { .header-tabs {
position: relative; display: grid;
display: flex; grid-template-columns: 1fr auto 1fr;
}
// MS Edge
@supports (-ms-ime-align: auto) {
.help-item {
align-self: center;
top: 1px;
}
} }
</style> </style>

View File

@@ -0,0 +1,97 @@
export default [
{
name: 'beffymaroo',
type: 'Staff',
uuid: '9fe7183a-4b79-4c15-9629-a1aee3873390',
},
// {
// name: 'lefnire',
// type: 'Staff',
// uuid: '00000000-0000-4000-9000-000000000000',
// },
{
name: 'Lemoness',
type: 'Staff',
uuid: '7bde7864-ebc5-4ee2-a4b7-1070d464cdb0',
},
{
name: 'paglias',
type: 'Staff',
uuid: 'ed4c688c-6652-4a92-9d03-a5a79844174a',
},
{
name: 'redphoenix',
type: 'Staff',
uuid: 'cb46ad54-8c78-4dbc-a8ed-4e3185b2b3ff',
},
{
name: 'SabreCat',
type: 'Staff',
uuid: '7f14ed62-5408-4e1b-be83-ada62d504931',
},
{
name: 'TheHollidayInn',
type: 'Staff',
uuid: '206039c6-24e4-4b9f-8a31-61cbb9aa3f66',
},
{
name: 'viirus',
type: 'Staff',
uuid: 'a327d7e0-1c2e-41be-9193-7b30b484413f',
},
{
name: 'It\'s Bailey',
type: 'Moderator',
uuid: '9da65443-ed43-4c21-804f-d260c1361596',
},
{
name: 'Alys',
type: 'Moderator',
uuid: 'd904bd62-da08-416b-a816-ba797c9ee265',
},
{
name: 'Blade',
type: 'Moderator',
uuid: '75f270e8-c5db-4722-a5e6-a83f1b23f76b',
},
{
name: 'Breadstrings',
type: 'Moderator',
uuid: '3b675c0e-d7a6-440c-8687-bc67cd0bf4e9',
},
{
name: 'Cantras',
type: 'Moderator',
uuid: '28771972-ca6d-4c03-8261-e1734aa7d21d',
},
// {
// name: 'Daniel the Bard',
// type: 'Moderator',
// uuid: '1f7c4a74-03a3-4b2c-b015-112d0acbd593',
// },
{
name: 'deilann 5.0.5b',
type: 'Moderator',
uuid: 'e7b5d1e2-3b6e-4192-b867-8bafdb03eeec',
},
{
name: 'Dewines',
type: 'Moderator',
uuid: '262a7afb-6b57-4d81-88e0-80d2e9f6cbdc',
},
{
name: 'Fox_town',
type: 'Moderator',
uuid: 'a05f0152-d66b-4ef1-93ac-4adb195d0031',
},
{
name: 'Megan',
type: 'Moderator',
uuid: '73e5125c-2c87-4004-8ccd-972aeac4f17a',
},
{
name: 'shanaqui',
type: 'Moderator',
uuid: 'bb089388-28ae-4e42-a8fa-f0c2bfb6f779',
},
];

View File

@@ -133,23 +133,6 @@ export default {
this.markdown(msg); // @TODO: mardown directive? this.markdown(msg); // @TODO: mardown directive?
// If using mpheal and there are other mages in the party, show extra notification
if (type === 'party' && spell.key === 'mpheal') {
// Counting mages
let magesCount = 0;
for (let i = 0; i < target.length; i++) {
if (target[i].stats.class === 'wizard') {
magesCount++;
}
}
// If there are mages, show message telling that the mpheal don't work on other mages
// The count must be bigger than 1 because the user casting the spell is a mage
if (magesCount > 1) {
this.markdown(this.$t('spellWizardNoEthOnMage'));
}
}
if (!beforeQuestProgress) return; if (!beforeQuestProgress) return;
let questProgress = this.questProgress() - beforeQuestProgress; let questProgress = this.questProgress() - beforeQuestProgress;
if (questProgress > 0) { if (questProgress > 0) {

View File

@@ -1,7 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import buyOp from 'common/script/ops/buy/buy'; import buyOp from 'common/script/ops/buy/buy';
import content from 'common/script/content/index'; import content from 'common/script/content/index';
import purchaseOp from 'common/script/ops/buy/purchaseWithSpell';
import hourglassPurchaseOp from 'common/script/ops/buy/hourglassPurchase'; import hourglassPurchaseOp from 'common/script/ops/buy/hourglassPurchase';
import sellOp from 'common/script/ops/sell'; import sellOp from 'common/script/ops/sell';
import unlockOp from 'common/script/ops/unlock'; import unlockOp from 'common/script/ops/unlock';
@@ -91,7 +90,7 @@ async function buyArmoire (store, params) {
export function purchase (store, params) { export function purchase (store, params) {
const quantity = params.quantity || 1; const quantity = params.quantity || 1;
const user = store.state.user.data; const user = store.state.user.data;
let opResult = purchaseOp(user, {params, quantity}); let opResult = buyOp(user, {params, quantity});
return { return {
result: opResult, result: opResult,

View File

@@ -11,8 +11,8 @@
"dailyDueDefaultView": "Set Dailies default to 'due' tab", "dailyDueDefaultView": "Set Dailies default to 'due' tab",
"dailyDueDefaultViewPop": "With this option set, the Dailies tasks will default to 'due' instead of 'all'", "dailyDueDefaultViewPop": "With this option set, the Dailies tasks will default to 'due' instead of 'all'",
"reverseChatOrder": "Show chat messages in reverse order", "reverseChatOrder": "Show chat messages in reverse order",
"startAdvCollapsed": "Advanced Options in tasks start collapsed", "startAdvCollapsed": "Advanced Settings in tasks start collapsed",
"startAdvCollapsedPop": "With this option set, Advanced Options will be hidden when you first open a task for editing.", "startAdvCollapsedPop": "With this option set, Advanced Settings will be hidden when you first open a task for editing.",
"dontShowAgain": "Don't show this again", "dontShowAgain": "Don't show this again",
"suppressLevelUpModal": "Don't show popup when gaining a level", "suppressLevelUpModal": "Don't show popup when gaining a level",
"suppressHatchPetModal": "Don't show popup when hatching a pet", "suppressHatchPetModal": "Don't show popup when hatching a pet",
@@ -85,7 +85,7 @@
"resetComplete": "Reset complete!", "resetComplete": "Reset complete!",
"fixValues": "Fix Values", "fixValues": "Fix Values",
"fixValuesText1": "If you've encountered a bug or made a mistake that unfairly changed your character (damage you shouldn't have taken, Gold you didn't really earn, etc.), you can manually correct your numbers here. Yes, this makes it possible to cheat: use this feature wisely, or you'll sabotage your own habit-building!", "fixValuesText1": "If you've encountered a bug or made a mistake that unfairly changed your character (damage you shouldn't have taken, Gold you didn't really earn, etc.), you can manually correct your numbers here. Yes, this makes it possible to cheat: use this feature wisely, or you'll sabotage your own habit-building!",
"fixValuesText2": "Note that you cannot restore Streaks on individual tasks here. To do that, edit the Daily and go to Advanced Options, where you will find a Restore Streak field.", "fixValuesText2": "Note that you cannot restore Streaks on individual tasks here. To do that, edit the Daily and go to Advanced Settings, where you will find a Restore Streak field.",
"disabledWinterEvent": "Disabled during Winter Wonderland Event Pt.4 (since the rewards are gold-purchaseable).", "disabledWinterEvent": "Disabled during Winter Wonderland Event Pt.4 (since the rewards are gold-purchaseable).",
"fix21Streaks": "21-Day Streaks", "fix21Streaks": "21-Day Streaks",
"discardChanges": "Discard Changes", "discardChanges": "Discard Changes",

View File

@@ -4,7 +4,6 @@
"spellWizardMPHealText": "Ethereal Surge", "spellWizardMPHealText": "Ethereal Surge",
"spellWizardMPHealNotes": "You sacrifice Mana so the rest of your Party, except Mages, gains MP! (Based on: INT)", "spellWizardMPHealNotes": "You sacrifice Mana so the rest of your Party, except Mages, gains MP! (Based on: INT)",
"spellWizardNoEthOnMage": "Your Skill backfires when mixed with another's magic. Only non-Mages gain MP.",
"spellWizardEarthText": "Earthquake", "spellWizardEarthText": "Earthquake",
"spellWizardEarthNotes": "Your mental power shakes the earth and buffs your Party's Intelligence! (Based on: Unbuffed INT)", "spellWizardEarthNotes": "Your mental power shakes the earth and buffs your Party's Intelligence! (Based on: Unbuffed INT)",

View File

@@ -5,7 +5,7 @@
"sureDeleteCompletedTodos": "Are you sure you want to delete your completed To-Dos?", "sureDeleteCompletedTodos": "Are you sure you want to delete your completed To-Dos?",
"lotOfToDos": "Your most recent 30 completed To-Dos are shown here. You can see older completed To-Dos from Data > Data Display Tool or Data > Export Data > User Data.", "lotOfToDos": "Your most recent 30 completed To-Dos are shown here. You can see older completed To-Dos from Data > Data Display Tool or Data > Export Data > User Data.",
"deleteToDosExplanation": "If you click the button below, all of your completed To-Dos and archived To-Dos will be permanently deleted, except for To-Dos from active challenges and Group Plans. Export them first if you want to keep a record of them.", "deleteToDosExplanation": "If you click the button below, all of your completed To-Dos and archived To-Dos will be permanently deleted, except for To-Dos from active challenges and Group Plans. Export them first if you want to keep a record of them.",
"addMultipleTip": "<strong>Tip:</strong> To add multiple Tasks, separate each one using a line break (Shift + Enter) and then press \"Enter.\"", "addMultipleTip": "<strong>Tip:</strong> To add multiple <%= taskType %>, separate each one using a line break (Shift + Enter) and then press \"Enter.\"",
"addsingle": "Add Single", "addsingle": "Add Single",
"addATask": "Add a <%= type %>", "addATask": "Add a <%= type %>",
"editATask": "Edit a <%= type %>", "editATask": "Edit a <%= type %>",
@@ -210,7 +210,6 @@
"yesterDailiesDescription": "If this setting is applied, Habitica will ask you if you meant to leave the Daily undone before calculating and applying damage to your avatar. This can protect you against unintentional damage.", "yesterDailiesDescription": "If this setting is applied, Habitica will ask you if you meant to leave the Daily undone before calculating and applying damage to your avatar. This can protect you against unintentional damage.",
"repeatDayError": "Please ensure that you have at least one day of the week selected.", "repeatDayError": "Please ensure that you have at least one day of the week selected.",
"searchTasks": "Search titles and descriptions...", "searchTasks": "Search titles and descriptions...",
"repeatDayError": "Please ensure that you have at least one day of the week selected.",
"sessionOutdated": "Your session is outdated. Please refresh or sync.", "sessionOutdated": "Your session is outdated. Please refresh or sync.",
"errorTemporaryItem": "This item is temporary and cannot be pinned." "errorTemporaryItem": "This item is temporary and cannot be pinned."
} }

View File

@@ -7,7 +7,7 @@ import {BuyHealthPotionOperation} from './buyHealthPotion';
import {BuyMarketGearOperation} from './buyMarketGear'; import {BuyMarketGearOperation} from './buyMarketGear';
import buyMysterySet from './buyMysterySet'; import buyMysterySet from './buyMysterySet';
import {BuyQuestWithGoldOperation} from './buyQuest'; import {BuyQuestWithGoldOperation} from './buyQuest';
import buySpecialSpell from './buySpecialSpell'; import {BuySpellOperation} from './buySpell';
import purchaseOp from './purchase'; import purchaseOp from './purchase';
import hourglassPurchase from './hourglassPurchase'; import hourglassPurchase from './hourglassPurchase';
import errorMessage from '../../libs/errorMessage'; import errorMessage from '../../libs/errorMessage';
@@ -70,9 +70,12 @@ module.exports = function buy (user, req = {}, analytics) {
buyRes = buyOp.purchase(); buyRes = buyOp.purchase();
break; break;
} }
case 'special': case 'special': {
buyRes = buySpecialSpell(user, req, analytics); const buyOp = new BuySpellOperation(user, req, analytics);
buyRes = buyOp.purchase();
break; break;
}
default: { default: {
const buyOp = new BuyMarketGearOperation(user, req, analytics); const buyOp = new BuyMarketGearOperation(user, req, analytics);

View File

@@ -25,6 +25,10 @@ export class BuyQuestWithGoldOperation extends AbstractGoldItemOperation {
user.achievements.quests.taskwoodsTerror3; user.achievements.quests.taskwoodsTerror3;
} }
getItemKey () {
return this.key;
}
getItemValue (item) { getItemValue (item) {
return item.goldValue; return item.goldValue;
} }
@@ -61,13 +65,4 @@ export class BuyQuestWithGoldOperation extends AbstractGoldItemOperation {
}), }),
]; ];
} }
analyticsData () {
return {
itemKey: this.key,
itemType: 'Market',
acquireMethod: 'Gold',
goldCost: this.getItemValue(this.item.goldValue),
};
}
} }

View File

@@ -1,48 +0,0 @@
import i18n from '../../i18n';
import content from '../../content/index';
import get from 'lodash/get';
import pick from 'lodash/pick';
import splitWhitespace from '../../libs/splitWhitespace';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../../libs/errors';
import errorMessage from '../../libs/errorMessage';
module.exports = function buySpecialSpell (user, req = {}, analytics) {
let key = get(req, 'params.key');
let quantity = req.quantity || 1;
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
let item = content.special[key];
if (!item) throw new NotFound(errorMessage('spellNotFound', {spellId: key}));
if (user.stats.gp < item.value * quantity) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
user.stats.gp -= item.value * quantity;
user.items.special[key] += quantity;
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: item.key,
itemType: 'Market',
goldCost: item.goldValue,
quantityPurchased: quantity,
acquireMethod: 'Gold',
category: 'behavior',
headers: req.headers,
});
}
return [
pick(user, splitWhitespace('items stats')),
i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language),
];
};

View File

@@ -0,0 +1,47 @@
import content from '../../content/index';
import get from 'lodash/get';
import pick from 'lodash/pick';
import splitWhitespace from '../../libs/splitWhitespace';
import {
BadRequest,
NotFound,
} from '../../libs/errors';
import {AbstractGoldItemOperation} from './abstractBuyOperation';
import errorMessage from '../../libs/errorMessage';
export class BuySpellOperation extends AbstractGoldItemOperation {
constructor (user, req, analytics) {
super(user, req, analytics);
}
getItemKey () {
return this.key;
}
multiplePurchaseAllowed () {
return true;
}
extractAndValidateParams (user, req) {
let key = this.key = get(req, 'params.key');
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
let item = content.special[key];
if (!item) throw new NotFound(errorMessage('spellNotFound', {spellId: key}));
this.canUserPurchase(user, item);
}
executeChanges (user, item, req) {
user.items.special[item.key] += this.quantity;
this.subtractCurrency(user, item, this.quantity);
return [
pick(user, splitWhitespace('items stats')),
this.i18n('messageBought', {
itemText: item.text(req.language),
}),
];
}
}

View File

@@ -1,12 +0,0 @@
import buy from './buy';
import get from 'lodash/get';
module.exports = function purchaseWithSpell (user, req = {}, analytics) {
const type = get(req.params, 'type');
if (type === 'spells') {
req.type = 'special';
}
return buy(user, req, analytics);
};

View File

@@ -1,7 +1,5 @@
import stripeModule from 'stripe';
import nconf from 'nconf';
import cc from 'coupon-code';
import moment from 'moment'; import moment from 'moment';
import logger from '../logger'; import logger from '../logger';
import { import {
BadRequest, BadRequest,
@@ -10,38 +8,22 @@ import {
} from '../errors'; } from '../errors';
import payments from './payments'; import payments from './payments';
import { model as User } from '../../models/user'; import { model as User } from '../../models/user';
import { model as Coupon } from '../../models/coupon';
import { import {
model as Group, model as Group,
basicFields as basicGroupFields, basicFields as basicGroupFields,
} from '../../models/group'; } from '../../models/group';
import shared from '../../../common'; import shared from '../../../common';
import stripeConstants from './stripe/constants';
import { checkout } from './stripe/checkout';
import { getStripeApi, setStripeApi } from './stripe/api';
let stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
const i18n = shared.i18n; const i18n = shared.i18n;
let api = {}; let api = {};
api.constants = { api.constants = Object.assign({}, stripeConstants);
// CURRENCY_CODE: 'USD',
// SELLER_NOTE: 'Habitica Payment',
// SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
// SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
// STORE_NAME: 'Habitica',
//
// GIFT_TYPE_GEMS: 'gems',
// GIFT_TYPE_SUBSCRIPTION: 'subscription',
//
// METHOD_BUY_GEMS: 'buyGems',
// METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
PAYMENT_METHOD: 'Stripe',
// PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
};
api.setStripeApi = function setStripeApi (stripeInc) {
stripe = stripeInc;
};
api.setStripeApi = setStripeApi;
/** /**
* Allows for purchasing a user subscription, group subscription or gems with Stripe * Allows for purchasing a user subscription, group subscription or gems with Stripe
@@ -56,110 +38,7 @@ api.setStripeApi = function setStripeApi (stripeInc) {
* @param options.headers The request headers to store on analytics * @param options.headers The request headers to store on analytics
* @return undefined * @return undefined
*/ */
api.checkout = async function checkout (options, stripeInc) { api.checkout = checkout;
let {
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
} = options;
let response;
let subscriptionId;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
let stripeApi = stripe;
if (stripeInc) stripeApi = stripeInc;
if (!token) throw new BadRequest('Missing req.body.id');
if (gift) {
const member = await User.findById(gift.uuid).exec();
gift.member = member;
}
if (sub) {
if (sub.discount) {
if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired'));
coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec();
if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon'));
}
let customerObject = {
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
};
if (groupId) {
customerObject.quantity = sub.quantity;
const groupFields = basicGroupFields.concat(' purchased');
const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
const membersCount = await group.getMemberCount();
customerObject.quantity = membersCount + sub.quantity - 1;
}
response = await stripeApi.customers.create(customerObject);
if (groupId) subscriptionId = response.subscriptions.data[0].id;
} else {
let amount = 500; // $5
if (gift) {
if (gift.type === 'subscription') {
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
} else {
if (gift.gems.amount <= 0) {
throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase'));
}
amount = `${gift.gems.amount / 4 * 100}`;
}
}
if (!gift || gift.type === 'gems') {
const receiver = gift ? gift.member : user;
const receiverCanGetGems = await receiver.canGetGems();
if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language));
}
response = await stripeApi.charges.create({
amount,
currency: 'usd',
card: token,
});
}
if (sub) {
await payments.createSubscription({
user,
customerId: response.id,
paymentMethod: this.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
subscriptionId,
});
} else {
let method = 'buyGems';
let data = {
user,
customerId: response.id,
paymentMethod: this.constants.PAYMENT_METHOD,
gift,
};
if (gift) {
if (gift.type === 'subscription') method = 'createSubscription';
data.paymentMethod = 'Gift';
}
await payments[method](data);
}
};
/** /**
* Edits a subscription created by Stripe * Edits a subscription created by Stripe
@@ -176,7 +55,7 @@ api.editSubscription = async function editSubscription (options, stripeInc) {
let customerId; let customerId;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
let stripeApi = stripe; let stripeApi = getStripeApi();
if (stripeInc) stripeApi = stripeInc; if (stripeInc) stripeApi = stripeInc;
if (groupId) { if (groupId) {
@@ -220,7 +99,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
let customerId; let customerId;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
let stripeApi = stripe; let stripeApi = getStripeApi();
if (stripeInc) stripeApi = stripeInc; if (stripeInc) stripeApi = stripeInc;
if (groupId) { if (groupId) {
@@ -271,7 +150,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
}; };
api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) { api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) {
let stripeApi = stripe; let stripeApi = getStripeApi();
let plan = shared.content.subscriptionBlocks.group_monthly; let plan = shared.content.subscriptionBlocks.group_monthly;
await stripeApi.subscriptions.update( await stripeApi.subscriptions.update(
@@ -298,7 +177,7 @@ api.handleWebhooks = async function handleWebhooks (options, stripeInc) {
let {requestBody} = options; let {requestBody} = options;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
let stripeApi = stripe; let stripeApi = getStripeApi();
if (stripeInc) stripeApi = stripeInc; if (stripeInc) stripeApi = stripeInc;
// Verify the event by fetching it from Stripe // Verify the event by fetching it from Stripe

View File

@@ -0,0 +1,14 @@
import stripeModule from 'stripe';
import nconf from 'nconf';
let stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
function setStripeApi (stripeInc) {
stripe = stripeInc;
}
function getStripeApi () {
return stripe;
}
module.exports = { getStripeApi, setStripeApi };

View File

@@ -0,0 +1,146 @@
import cc from 'coupon-code';
import { getStripeApi } from './api';
import { model as User } from '../../../models/user';
import { model as Coupon } from '../../../models/coupon';
import {
model as Group,
basicFields as basicGroupFields,
} from '../../../models/group';
import shared from '../../../../common';
import {
BadRequest,
NotAuthorized,
} from '../../errors';
import payments from './../payments';
import stripeConstants from './constants';
function getGiftAmount (gift) {
if (gift.type === 'subscription') {
return `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
}
if (gift.gems.amount <= 0) {
throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase'));
}
return `${gift.gems.amount / 4 * 100}`;
}
async function buyGems (gift, user, token, stripeApi) {
let amount = 500; // $5
if (gift) amount = getGiftAmount(gift);
if (!gift || gift.type === 'gems') {
const receiver = gift ? gift.member : user;
const receiverCanGetGems = await receiver.canGetGems();
if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language));
}
const response = await stripeApi.charges.create({
amount,
currency: 'usd',
card: token,
});
return response;
}
async function buySubscription (sub, coupon, email, user, token, groupId, stripeApi) {
if (sub.discount) {
if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired'));
coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec();
if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon'));
}
let customerObject = {
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
};
if (groupId) {
customerObject.quantity = sub.quantity;
const groupFields = basicGroupFields.concat(' purchased');
const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
const membersCount = await group.getMemberCount();
customerObject.quantity = membersCount + sub.quantity - 1;
}
const response = await stripeApi.customers.create(customerObject);
let subscriptionId;
if (groupId) subscriptionId = response.subscriptions.data[0].id;
return { subResponse: response, subId: subscriptionId };
}
async function applyGemPayment (user, response, gift) {
let method = 'buyGems';
const data = {
user,
customerId: response.id,
paymentMethod: stripeConstants.PAYMENT_METHOD,
gift,
};
if (gift) {
if (gift.type === 'subscription') method = 'createSubscription';
data.paymentMethod = 'Gift';
}
await payments[method](data);
}
async function checkout (options, stripeInc) {
let {
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
} = options;
let response;
let subscriptionId;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
let stripeApi = getStripeApi();
if (stripeInc) stripeApi = stripeInc;
if (!token) throw new BadRequest('Missing req.body.id');
if (gift) {
const member = await User.findById(gift.uuid).exec();
gift.member = member;
}
if (sub) {
const { subId, subResponse } = await buySubscription(sub, coupon, email, user, token, groupId, stripeApi);
subscriptionId = subId;
response = subResponse;
} else {
response = await buyGems(gift, user, token, stripeApi);
}
if (sub) {
await payments.createSubscription({
user,
customerId: response.id,
paymentMethod: this.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
subscriptionId,
});
return;
}
await applyGemPayment(user, response, gift);
}
module.exports = { checkout };

View File

@@ -0,0 +1,15 @@
module.exports = {
// CURRENCY_CODE: 'USD',
// SELLER_NOTE: 'Habitica Payment',
// SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
// SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
// STORE_NAME: 'Habitica',
//
// GIFT_TYPE_GEMS: 'gems',
// GIFT_TYPE_SUBSCRIPTION: 'subscription',
//
// METHOD_BUY_GEMS: 'buyGems',
// METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
PAYMENT_METHOD: 'Stripe',
// PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
};

View File

@@ -1,11 +1,11 @@
import nconf from 'nconf'; import nconf from 'nconf';
import url from 'url';
const IS_PROD = nconf.get('IS_PROD'); const IS_PROD = nconf.get('IS_PROD');
const IGNORE_REDIRECT = nconf.get('IGNORE_REDIRECT') === 'true'; const IGNORE_REDIRECT = nconf.get('IGNORE_REDIRECT') === 'true';
const BASE_URL = nconf.get('BASE_URL'); const BASE_URL = nconf.get('BASE_URL');
let baseUrlSplit = BASE_URL.split('//'); const BASE_URL_HOST = url.parse(BASE_URL).hostname;
const BASE_URL_HOST = baseUrlSplit[1];
function isHTTP (req) { function isHTTP (req) {
return ( // eslint-disable-line no-extra-parens return ( // eslint-disable-line no-extra-parens

View File

@@ -346,7 +346,11 @@ schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, use
// Convert to timestamps because Android expects it // Convert to timestamps because Android expects it
toJSON.chat.forEach(chat => { toJSON.chat.forEach(chat => {
chat.timestamp = chat.timestamp ? chat.timestamp.getTime() : new Date().getTime(); // old chats are saved with a numeric timestamp
// new chats use `Date` which then has to be converted to the numeric timestamp
if (chat.timestamp && chat.timestamp.getTime) {
chat.timestamp = chat.timestamp.getTime();
}
}); });
return toJSON; return toJSON;