From 7ee49a43f2efa97886a8ce10f965a89582b4605e Mon Sep 17 00:00:00 2001 From: Alys Date: Sat, 30 Sep 2017 06:56:44 +1000 Subject: [PATCH 1/4] Various fixes: mods can delete any message; no Report / CopyAsTodo button in inbox; etc (#9091) * fix link to Data Display Tool * prevent the Copy As To-Do button appearing in the inbox because it isn't working * allow mods/staff to delete any chat message * prevent the Flag/Report button appearing in the inbox because it isn't working --- website/client/components/appFooter.vue | 2 +- website/client/components/chat/chatMessages.vue | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/website/client/components/appFooter.vue b/website/client/components/appFooter.vue index 5b99719461..af758df868 100644 --- a/website/client/components/appFooter.vue +++ b/website/client/components/appFooter.vue @@ -55,7 +55,7 @@ li a(href='/apidoc', target='_blank') {{ $t('APIv3') }} li - a(href='http://data.habitrpg.com/?uuid=', target='_blank') {{ $t('dataDisplayTool') }} + a(href='https://oldgods.net/habitrpg/habitrpg_user_data_display.html', target='_blank') {{ $t('dataDisplayTool') }} li a(href='http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths', target='_blank') {{ $t('guidanceForBlacksmiths') }} li diff --git a/website/client/components/chat/chatMessages.vue b/website/client/components/chat/chatMessages.vue index 4eae6e5f7f..fdaca67f42 100644 --- a/website/client/components/chat/chatMessages.vue +++ b/website/client/components/chat/chatMessages.vue @@ -36,13 +36,15 @@ .svg-icon(v-html="icons.like") span(v-if='!msg.likes[user._id]') {{ $t('like') }} span(v-if='msg.likes[user._id]') {{ $t('liked') }} - span.action( @click='copyAsTodo(msg)') + span.action(v-if='!inbox', @click='copyAsTodo(msg)') .svg-icon(v-html="icons.copy") | {{$t('copyAsTodo')}} - span.action(v-if='user.contributor.admin || (msg.uuid !== user._id && user.flags.communityGuidelinesAccepted)', @click='report(msg)') + // @TODO make copyAsTodo work in the inbox + span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted', @click='report(msg)') .svg-icon(v-html="icons.report") | {{$t('report')}} - span.action(v-if='msg.uuid === user._id || inbox', @click='remove(msg, index)') + // @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys + span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove(msg, index)') .svg-icon(v-html="icons.delete") | {{$t('delete')}} span.action.float-right.liked(v-if='likeCount(msg) > 0') @@ -70,12 +72,15 @@ .svg-icon(v-html="icons.like") span(v-if='!msg.likes[user._id]') {{ $t('like') }} span(v-if='msg.likes[user._id]') {{ $t('liked') }} - span.action( @click='copyAsTodo(msg)') + span.action(v-if='!inbox', @click='copyAsTodo(msg)') .svg-icon(v-html="icons.copy") | {{$t('copyAsTodo')}} + // @TODO make copyAsTodo work in the inbox span.action(v-if='user.flags.communityGuidelinesAccepted', @click='report(msg)') + span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted', @click='report(msg)') .svg-icon(v-html="icons.report") | {{$t('report')}} + // @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys span.action(v-if='msg.uuid === user._id', @click='remove(msg, index)') .svg-icon(v-html="icons.delete") | {{$t('delete')}} From 1d8c126687d65a65e0b25396389adfd5d7916248 Mon Sep 17 00:00:00 2001 From: Alys Date: Sat, 30 Sep 2017 06:56:54 +1000 Subject: [PATCH 2/4] hide the Block button in the user's own profile screen (#9098) This prevents a user from accidentally blocking themself from sending PMs. --- website/client/components/userMenu/profile.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/client/components/userMenu/profile.vue b/website/client/components/userMenu/profile.vue index bdfa5ccaff..f88288623d 100644 --- a/website/client/components/userMenu/profile.vue +++ b/website/client/components/userMenu/profile.vue @@ -5,7 +5,7 @@ div .profile-actions button.btn.btn-secondary(@click='sendMessage()') .svg-icon.message-icon(v-html="icons.message") - button.btn.btn-secondary(v-if='userLoggedIn.inbox.blocks.indexOf(user._id) === -1', :tooltip="$t('unblock')", + button.btn.btn-secondary(v-if='user._id !== this.userLoggedIn._id && userLoggedIn.inbox.blocks.indexOf(user._id) === -1', :tooltip="$t('unblock')", @click="blockUser()", tooltip-placement='right') span.glyphicon.glyphicon-plus | {{$t('block')}} From dc3a02bc2ef7e059e59498040b8a2c86e8a2a0b0 Mon Sep 17 00:00:00 2001 From: Alys Date: Sat, 30 Sep 2017 06:57:05 +1000 Subject: [PATCH 3/4] clarify change class code and remove incorrect confirmation step (#9099) - prevent the user seeing a confirmation about paying 3 gems when enabling the class system (which is free) - rename changeClass function to changeClassForUser to avoid confusion with changeClass function - make change class confirmation translatable - changed "gems" to "Gems" in locales string --- website/client/components/settings/site.vue | 12 ++++++------ website/common/locales/en/character.json | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/website/client/components/settings/site.vue b/website/client/components/settings/site.vue index 24957203d6..73691dfb1c 100644 --- a/website/client/components/settings/site.vue +++ b/website/client/components/settings/site.vue @@ -36,9 +36,9 @@ h6(v-once) {{ $t('class') + ': ' }} // @TODO: what is classText span(v-if='classText') {{ classText }}  - button.btn.btn-danger.btn-xs(@click='changeClass(null)', v-once) {{ $t('changeClass') }} - small.cost 3 - span.Pet_Currency_Gem1x.inline-gems + button.btn.btn-danger.btn-xs(@click='changeClassForUser(true)', v-once) {{ $t('changeClass') }} + small.cost   3 {{ $t('gems') }} + // @TODO add icon span.Pet_Currency_Gem1x.inline-gems hr div @@ -82,7 +82,7 @@ button.btn.btn-primary(@click='showBailey()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('showBaileyPop')") {{ $t('showBailey') }} button.btn.btn-primary(@click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('fixValPop')") {{ $t('fixVal') }} - button.btn.btn-primary(v-if='user.preferences.disableClasses == true', @click='changeClass({})', + button.btn.btn-primary(v-if='user.preferences.disableClasses == true', @click='changeClassForUser(false)', popover-trigger='mouseenter', popover-placement='right', :popover="$t('enableClassPop')") {{ $t('enableClass') }} hr @@ -378,8 +378,8 @@ export default { this.$router.go('/tasks'); }, - async changeClass () { - if (!confirm('Are you sure you want to change your class for 3 gems?')) return; + async changeClassForUser (confirmationNeeded) { + if (confirmationNeeded && !confirm(this.$t('changeClassConfirmCost'))) return; try { changeClass(this.user); await axios.post('/api/v3/user/change-class'); diff --git a/website/common/locales/en/character.json b/website/common/locales/en/character.json index 9c6aec8c20..d74a9cf26a 100644 --- a/website/common/locales/en/character.json +++ b/website/common/locales/en/character.json @@ -125,6 +125,7 @@ "mystery": "Mystery", "changeClass": "Change Class, Refund Attribute Points", "lvl10ChangeClass": "To change class you must be at least level 10.", + "changeClassConfirmCost": "Are you sure you want to change your class for 3 Gems?", "invalidClass":"Invalid class. Please specify 'warrior', 'rogue', 'wizard', or 'healer'.", "levelPopover": "Each level earns you one point to assign to an attribute of your choice. You can do so manually, or let the game decide for you using one of the Automatic Allocation options.", "unallocated": "Unallocated Attribute Points", @@ -158,7 +159,7 @@ "respawn": "Respawn!", "youDied": "You Died!", "dieText": "You've lost a Level, all your Gold, and a random piece of Equipment. Arise, Habiteer, and try again! Curb those negative Habits, be vigilant in completion of Dailies, and hold death at arm's length with a Health Potion if you falter!", - "sureReset": "Are you sure? This will reset your character's class and allocated points (you'll get them all back to re-allocate), and costs 3 gems.", + "sureReset": "Are you sure? This will reset your character's class and allocated points (you'll get them all back to re-allocate), and costs 3 Gems.", "purchaseFor": "Purchase for <%= cost %> Gems?", "notEnoughMana": "Not enough mana.", "invalidTarget": "You can't cast a skill on that.", From 7671347d3a4f7f1b7bcd04218acc34d87c93ee35 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Fri, 29 Sep 2017 16:03:43 -0500 Subject: [PATCH 4/4] Chat avatar fixes (#9103) * Ensured rejection doesn't hurt performance * Added debounce and scroll removal * Added debounce for keydown * Fixed linting --- website/client/app.vue | 11 ++++- .../client/components/chat/chatMessages.vue | 44 +++++++++++++------ website/client/components/groups/group.vue | 6 ++- website/client/components/groups/tavern.vue | 6 ++- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/website/client/app.vue b/website/client/app.vue index cae0f46a12..510b69089e 100644 --- a/website/client/app.vue +++ b/website/client/app.vue @@ -138,12 +138,21 @@ export default { // @TODO split up this file, it's too big // Set up Error interceptors axios.interceptors.response.use((response) => { - if (this.user) { + if (this.user && response.data && response.data.notifications) { this.$set(this.user, 'notifications', response.data.notifications); } return response; }, (error) => { if (error.response.status >= 400) { + // Don't show errors from getting user details. These users have delete their account, + // but their chat message still exists. + let configExists = Boolean(error.response) && Boolean(error.response.config); + if (configExists && error.response.config.method === 'get' && error.response.config.url.indexOf('/api/v3/members/') !== -1) { + // @TODO: We resolve the promise because we need our caching to cache this user as tried + // Chat paging should help this, but maybe we can also find another solution.. + return Promise.resolve(error); + } + this.$store.state.notificationStore.push({ title: 'Habitica', text: error.response.data.message, diff --git a/website/client/components/chat/chatMessages.vue b/website/client/components/chat/chatMessages.vue index fdaca67f42..9396e2e809 100644 --- a/website/client/components/chat/chatMessages.vue +++ b/website/client/components/chat/chatMessages.vue @@ -5,14 +5,14 @@ copy-as-todo-modal(:copying-message='copyingMessage', :group-name='groupName', :group-id='groupId') report-flag-modal - div(v-for="(msg, index) in chat", v-if='chat && canViewFlag(msg)') + div(v-for="(msg, index) in messages", v-if='chat && canViewFlag(msg)') // @TODO: is there a different way to do these conditionals? This creates an infinite loop //.hr(v-if='displayDivider(msg)') .hr-middle(v-once) {{ msg.timestamp }} .row(v-if='user._id !== msg.uuid') div(:class='inbox ? "col-4" : "col-2"') avatar( - v-if='cachedProfileData[msg.uuid]', + v-if='cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected', :member="cachedProfileData[msg.uuid]", :avatarOnly="true", :hideClassBadge='true', @@ -89,7 +89,7 @@ | + {{ likeCount(msg) }} div(:class='inbox ? "col-4" : "col-2"') avatar( - v-if='cachedProfileData[msg.uuid]', + v-if='cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected', :member="cachedProfileData[msg.uuid]", :avatarOnly="true", :hideClassBadge='true', @@ -243,7 +243,7 @@ import axios from 'axios'; import moment from 'moment'; import cloneDeep from 'lodash/cloneDeep'; import { mapState } from 'client/libs/store'; -import throttle from 'lodash/throttle'; +import debounce from 'lodash/debounce'; import markdownDirective from 'client/directives/markdown'; import Avatar from '../avatar'; import styleHelper from 'client/mixins/styleHelper'; @@ -282,12 +282,10 @@ export default { this.loadProfileCache(); }, created () { - window.addEventListener('scroll', throttle(() => { - this.loadProfileCache(window.scrollY / 1000); - }, 1000)); + window.addEventListener('scroll', this.handleScroll); }, destroyed () { - // window.removeEventListener('scroll', this.handleScroll); + window.removeEventListener('scroll', this.handleScroll); }, data () { return { @@ -313,6 +311,7 @@ export default { cachedProfileData: {}, currentProfileLoadedCount: 0, currentProfileLoadedEnd: 10, + loading: false, }; }, filters: { @@ -326,17 +325,22 @@ export default { }, computed: { ...mapState({user: 'user.data'}), + // @TODO: We need a different lazy load mechnism. + // But honestly, adding a paging route to chat would solve this messages () { return this.chat; }, }, watch: { - messages () { - // @TODO: MAybe we should watch insert and remove? + messages (oldValue, newValue) { + if (newValue.length === oldValue.length) return; this.loadProfileCache(); }, }, methods: { + handleScroll () { + this.loadProfileCache(window.scrollY / 1000); + }, isUserMentioned (message) { let user = this.user; @@ -363,7 +367,10 @@ export default { if (!message.flagCount || message.flagCount < 2) return true; return this.user.contributor.admin; }, - async loadProfileCache (screenPosition) { + loadProfileCache: debounce(function loadProfileCache (screenPosition) { + this._loadProfileCache(screenPosition); + }, 1000), + async _loadProfileCache (screenPosition) { let promises = []; // @TODO: write an explination @@ -373,11 +380,10 @@ export default { return; } - // @TODO: Not sure we need this hash let aboutToCache = {}; this.messages.forEach(message => { let uuid = message.uuid; - if (uuid && !this.cachedProfileData[uuid] && !aboutToCache[uuid]) { + if (Boolean(uuid) && !this.cachedProfileData[uuid] && !aboutToCache[uuid]) { if (uuid === 'system' || this.currentProfileLoadedCount === this.currentProfileLoadedEnd) return; aboutToCache[uuid] = {}; promises.push(axios.get(`/api/v3/members/${uuid}`)); @@ -387,9 +393,21 @@ export default { let results = await Promise.all(promises); results.forEach(result => { + // We could not load the user. Maybe they were deleted. So, let's cache empty so we don't try again + if (!result || !result.data || result.status >= 400) { + return; + } + let userData = result.data.data; this.$set(this.cachedProfileData, userData._id, userData); }); + + // Merge in any attempts that were rejected so we don't attempt again + for (let uuid in aboutToCache) { + if (!this.cachedProfileData[uuid]) { + this.$set(this.cachedProfileData, uuid, {rejected: true}); + } + } }, displayDivider (message) { if (this.currentDayDividerDisplay !== moment(message.timestamp).day()) { diff --git a/website/client/components/groups/group.vue b/website/client/components/groups/group.vue index c02fd1f518..0744bbb1a5 100644 --- a/website/client/components/groups/group.vue +++ b/website/client/components/groups/group.vue @@ -431,6 +431,7 @@