mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 13:17:24 +01:00
Performance: Inbox Paging / loading (#11157)
* load messages per conversation * only sort ones in ui * add contributor to message * fix correct message layout/message * mugenScroll on chatMessages * fix lint, no mugen-scroll, use own scroll handler * fix height / margin of modal + use button to load more * fix tests * user data from inbox * style "load earlier messages" * move mapMessage to the inbox api result / extract sentMessage of members-api-controller * fix test back * fix test * keep last scroll position * just set the Id of the returned message instead of all other properties * fix add new messages (buttons were hidden) + load more * item-mounted debounce to trigger the re-scrolling
This commit is contained in:
@@ -6,7 +6,7 @@ describe('GET /inbox/messages', () => {
|
||||
let user;
|
||||
let otherUser;
|
||||
|
||||
before(async () => {
|
||||
beforeEach(async () => {
|
||||
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
|
||||
|
||||
await otherUser.post('/members/send-private-message', {
|
||||
|
||||
@@ -7,7 +7,7 @@ describe('GET /inbox/conversations', () => {
|
||||
let otherUser;
|
||||
let thirdUser;
|
||||
|
||||
before(async () => {
|
||||
beforeEach(async () => {
|
||||
[user, otherUser, thirdUser] = await Promise.all([generateUser(), generateUser(), generateUser()]);
|
||||
|
||||
await otherUser.post('/members/send-private-message', {
|
||||
@@ -41,4 +41,51 @@ describe('GET /inbox/conversations', () => {
|
||||
expect(result[0].user).to.be.equal(user.profile.name);
|
||||
expect(result[0].username).to.be.equal(user.auth.local.username);
|
||||
});
|
||||
|
||||
it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => {
|
||||
const messages = await user.get('/inbox/messages');
|
||||
|
||||
expect(messages.length).to.equal(5);
|
||||
|
||||
// message to yourself
|
||||
expect(messages[0].text).to.equal('fifth');
|
||||
expect(messages[0].sent).to.equal(false);
|
||||
expect(messages[0].uuid).to.equal(user._id);
|
||||
|
||||
expect(messages[1].text).to.equal('fourth');
|
||||
expect(messages[2].text).to.equal('third');
|
||||
expect(messages[3].text).to.equal('second');
|
||||
expect(messages[4].text).to.equal('first');
|
||||
});
|
||||
|
||||
it('returns four messages when using page-query ', async () => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(user.post('/members/send-private-message', {
|
||||
toUserId: user.id,
|
||||
message: 'fourth',
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const messages = await user.get('/inbox/messages?page=1');
|
||||
|
||||
expect(messages.length).to.equal(5);
|
||||
});
|
||||
|
||||
it('returns only the messages of one conversation', async () => {
|
||||
const messages = await user.get(`/inbox/messages?conversation=${otherUser.id}`);
|
||||
|
||||
expect(messages.length).to.equal(3);
|
||||
});
|
||||
|
||||
it('returns the correct message format', async () => {
|
||||
const messages = await otherUser.get(`/inbox/messages?conversation=${user.id}`);
|
||||
|
||||
expect(messages[0].toUUID).to.equal(user.id); // from user
|
||||
expect(messages[1].toUUID).to.not.exist; // only filled if its from the chat partner
|
||||
expect(messages[2].toUUID).to.equal(user.id); // from user
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('POST /members/flag-private-message/:messageId', () => {
|
||||
let senderMessages = await userToSendMessage.get('/inbox/messages');
|
||||
|
||||
let sendersMessageInSendersInbox = _.find(senderMessages, (message) => {
|
||||
return message.uuid === receiver._id && message.text === messageToSend;
|
||||
return message.toUUID === receiver._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
|
||||
@@ -273,5 +273,8 @@ export default {
|
||||
return habiticaMarkdown.render(String(text));
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$emit('item-mounted', this.msg.id);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template lang="pug">
|
||||
.container-fluid
|
||||
.container-fluid(ref="container")
|
||||
.row
|
||||
.col-12
|
||||
copy-as-todo-modal(:group-type='groupType', :group-name='groupName', :group-id='groupId')
|
||||
.row.loadmore
|
||||
div(v-if="canLoadMore")
|
||||
.loadmore-divider
|
||||
button.btn.btn-secondary(@click='triggerLoad()') {{ $t('loadEarlierMessages') }}
|
||||
.loadmore-divider
|
||||
h2.col-12.loading(v-show="isLoading") {{ $t('loading') }}
|
||||
div(v-for="(msg, index) in messages", v-if='chat && canViewFlag(msg)', :class='{row: inbox}')
|
||||
.d-flex(v-if='user._id !== msg.uuid', :class='{"flex-grow-1": inbox}')
|
||||
avatar.avatar-left(
|
||||
@@ -21,7 +27,8 @@
|
||||
:groupId='groupId',
|
||||
@message-liked='messageLiked',
|
||||
@message-removed='messageRemoved',
|
||||
@show-member-modal='showMemberModal')
|
||||
@show-member-modal='showMemberModal',
|
||||
@item-mounted='itemWasMounted')
|
||||
.d-flex(v-if='user._id === msg.uuid', :class='{"flex-grow-1": inbox}')
|
||||
.card(:class='{"col-10": inbox}')
|
||||
chat-card(
|
||||
@@ -30,7 +37,8 @@
|
||||
:groupId='groupId',
|
||||
@message-liked='messageLiked',
|
||||
@message-removed='messageRemoved',
|
||||
@show-member-modal='showMemberModal')
|
||||
@show-member-modal='showMemberModal',
|
||||
@item-mounted='itemWasMounted')
|
||||
avatar(
|
||||
v-if='msg.userStyles || (cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected)',
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid]",
|
||||
@@ -49,6 +57,34 @@
|
||||
width: 10%;
|
||||
min-width: 7rem;
|
||||
}
|
||||
.loadmore {
|
||||
justify-content: center;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
text-align: center;
|
||||
color: $gray-50;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loadmore-divider {
|
||||
height: 1px;
|
||||
background-color: $gray-500;
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-left {
|
||||
margin-left: -1.5rem;
|
||||
@@ -97,6 +133,8 @@
|
||||
.message-scroll .d-flex {
|
||||
min-width: 1px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -120,6 +158,9 @@ export default {
|
||||
groupType: {},
|
||||
groupId: {},
|
||||
groupName: {},
|
||||
|
||||
isLoading: Boolean,
|
||||
canLoadMore: Boolean,
|
||||
},
|
||||
components: {
|
||||
copyAsTodoModal,
|
||||
@@ -142,6 +183,8 @@ export default {
|
||||
currentProfileLoadedCount: 0,
|
||||
currentProfileLoadedEnd: 10,
|
||||
loading: false,
|
||||
handleScrollBack: false,
|
||||
lastOffset: -1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -153,15 +196,24 @@ export default {
|
||||
return this.chat;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
messages () {
|
||||
this.loadProfileCache();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleScroll () {
|
||||
this.loadProfileCache(window.scrollY / 1000);
|
||||
},
|
||||
async triggerLoad () {
|
||||
const container = this.$refs.container;
|
||||
|
||||
// get current offset
|
||||
this.lastOffset = container.scrollTop - (container.scrollHeight - container.clientHeight);
|
||||
// disable scroll
|
||||
container.style.overflowY = 'hidden';
|
||||
|
||||
const canLoadMore = this.inbox && !this.isLoading && this.canLoadMore;
|
||||
if (canLoadMore) {
|
||||
await this.$emit('triggerLoad');
|
||||
this.handleScrollBack = true;
|
||||
}
|
||||
},
|
||||
canViewFlag (message) {
|
||||
if (message.uuid === this.user._id) return true;
|
||||
if (!message.flagCount || message.flagCount < 2) return true;
|
||||
@@ -252,6 +304,20 @@ export default {
|
||||
this.$router.push({name: 'userProfile', params: {userId: profile._id}});
|
||||
}
|
||||
},
|
||||
itemWasMounted: debounce(function itemWasMounted () {
|
||||
if (this.handleScrollBack) {
|
||||
this.handleScrollBack = false;
|
||||
|
||||
const container = this.$refs.container;
|
||||
const offset = container.scrollHeight - container.clientHeight;
|
||||
|
||||
const newOffset = offset + this.lastOffset;
|
||||
|
||||
container.scrollTo(0, newOffset);
|
||||
// enable scroll again
|
||||
container.style.overflowY = 'scroll';
|
||||
}
|
||||
}, 50),
|
||||
messageLiked (message) {
|
||||
const chatIndex = findIndex(this.chat, chatMessage => {
|
||||
return chatMessage.id === message.id;
|
||||
|
||||
@@ -34,21 +34,25 @@
|
||||
.svg-icon(v-html="tierIcon(conversation)")
|
||||
.time
|
||||
span.mr-1(v-if='conversation.username') @{{ conversation.username }} •
|
||||
span {{ conversation.date | timeAgo }}
|
||||
span(v-if="conversation.date") {{ conversation.date | timeAgo }}
|
||||
div.messagePreview {{ conversation.lastMessageText ? removeTags(parseMarkdown(conversation.lastMessageText)) : '' }}
|
||||
.col-8.messages.d-flex.flex-column.justify-content-between
|
||||
.empty-messages.text-center(v-if='!selectedConversation.key')
|
||||
.svg-icon.envelope(v-html="icons.messageIcon")
|
||||
h4 {{placeholderTexts.title}}
|
||||
p(v-html="placeholderTexts.description")
|
||||
.empty-messages.text-center(v-if='selectedConversation.key && selectedConversationMessages.length === 0')
|
||||
.empty-messages.text-center(v-if='selectedConversation && selectedConversationMessages.length === 0')
|
||||
p {{ $t('beginningOfConversation', {userName: selectedConversation.name})}}
|
||||
chat-messages.message-scroll(
|
||||
v-if="selectedConversation.messages && selectedConversationMessages.length > 0",
|
||||
v-if="selectedConversation && selectedConversationMessages.length > 0",
|
||||
:chat='selectedConversationMessages',
|
||||
:inbox='true',
|
||||
@message-removed='messageRemoved',
|
||||
ref="chatscroll"
|
||||
ref="chatscroll",
|
||||
|
||||
:canLoadMore="canLoadMore",
|
||||
:isLoading="messagesLoading",
|
||||
@triggerLoad="infiniteScrollTrigger"
|
||||
)
|
||||
.pm-disabled-caption.text-center(v-if="user.inbox.optOut && selectedConversation.key")
|
||||
h4 {{$t('PMDisabledCaptionTitle')}}
|
||||
@@ -64,6 +68,12 @@
|
||||
span.ml-3 {{ currentLength }} / 3000
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#inbox-modal .modal-body {
|
||||
padding-top: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/tiers.scss';
|
||||
@@ -94,7 +104,7 @@
|
||||
|
||||
.sidebar {
|
||||
background-color: $gray-700;
|
||||
min-height: 600px;
|
||||
min-height: 540px;
|
||||
padding: 0;
|
||||
|
||||
.search-section {
|
||||
@@ -107,6 +117,7 @@
|
||||
position: relative;
|
||||
padding-left: 0;
|
||||
padding-bottom: 6em;
|
||||
height: 540px;
|
||||
}
|
||||
|
||||
.message-scroll {
|
||||
@@ -225,8 +236,8 @@
|
||||
import Vue from 'vue';
|
||||
import moment from 'moment';
|
||||
import filter from 'lodash/filter';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
@@ -308,8 +319,14 @@ export default {
|
||||
newMessage: '',
|
||||
showPopover: false,
|
||||
messages: [],
|
||||
messagesByConversation: {}, // cache {uuid: []}
|
||||
loadedConversations: [],
|
||||
loaded: false,
|
||||
messagesLoading: false,
|
||||
canLoadMore: true,
|
||||
page: 0,
|
||||
initiatedConversation: null,
|
||||
updateConversionsCounter: 0,
|
||||
};
|
||||
},
|
||||
filters: {
|
||||
@@ -320,7 +337,7 @@ export default {
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
conversations () {
|
||||
const inboxGroup = groupBy(this.messages, 'uuid');
|
||||
const inboxGroup = groupBy(this.loadedConversations, 'uuid');
|
||||
|
||||
// Add placeholder for new conversations
|
||||
if (this.initiatedConversation && this.initiatedConversation.uuid) {
|
||||
@@ -328,6 +345,7 @@ export default {
|
||||
uuid: this.initiatedConversation.uuid,
|
||||
user: this.initiatedConversation.user,
|
||||
username: this.initiatedConversation.username,
|
||||
contributor: this.initiatedConversation.contributor,
|
||||
id: '',
|
||||
text: '',
|
||||
timestamp: new Date(),
|
||||
@@ -336,31 +354,7 @@ export default {
|
||||
// Create conversation objects
|
||||
const convos = [];
|
||||
for (let key in inboxGroup) {
|
||||
const convoSorted = sortBy(inboxGroup[key], [(o) => {
|
||||
return (new Date(o.timestamp)).getTime();
|
||||
}]);
|
||||
|
||||
// Fix poor inbox chat models
|
||||
const newChatModels = convoSorted.map(chat => {
|
||||
let newChat = Object.assign({}, chat);
|
||||
if (newChat.sent) {
|
||||
newChat.toUUID = newChat.uuid;
|
||||
newChat.toUser = newChat.user;
|
||||
newChat.toUserName = newChat.username;
|
||||
newChat.toUserContributor = newChat.contributor;
|
||||
newChat.toUserBacker = newChat.backer;
|
||||
newChat.uuid = this.user._id;
|
||||
newChat.user = this.user.profile.name;
|
||||
newChat.username = this.user.auth.local.username;
|
||||
newChat.contributor = this.user.contributor;
|
||||
newChat.backer = this.user.backer;
|
||||
}
|
||||
return newChat;
|
||||
});
|
||||
|
||||
// In case the last message is a placeholder, remove it
|
||||
const recentMessage = newChatModels[newChatModels.length - 1];
|
||||
if (!recentMessage.text) newChatModels.splice(newChatModels.length - 1, 1);
|
||||
const recentMessage = inboxGroup[key][0];
|
||||
|
||||
const convoModel = {
|
||||
key: recentMessage.toUUID ? recentMessage.toUUID : recentMessage.uuid,
|
||||
@@ -368,30 +362,46 @@ export default {
|
||||
username: !recentMessage.text ? recentMessage.username : recentMessage.toUserName,
|
||||
date: recentMessage.timestamp,
|
||||
lastMessageText: recentMessage.text,
|
||||
messages: newChatModels,
|
||||
};
|
||||
|
||||
convos.push(convoModel);
|
||||
}
|
||||
|
||||
// Sort models by most recent
|
||||
const conversations = sortBy(convos, [(o) => {
|
||||
return moment(o.date).toDate();
|
||||
}]);
|
||||
|
||||
return conversations.reverse();
|
||||
return convos;
|
||||
},
|
||||
// Separate from selectedConversation which is not coputed so messages don't update automatically
|
||||
// Separate from selectedConversation which is not computed so messages don't update automatically
|
||||
selectedConversationMessages () {
|
||||
// Vue-subscribe to changes
|
||||
const subScribeToUpdate = this.messagesLoading || this.updateConversionsCounter > -1;
|
||||
|
||||
|
||||
const selectedConversationKey = this.selectedConversation.key;
|
||||
const selectedConversation = this.conversations.find(c => c.key === selectedConversationKey);
|
||||
return selectedConversation ? selectedConversation.messages : [];
|
||||
const selectedConversation = this.messagesByConversation[selectedConversationKey];
|
||||
this.messages = selectedConversation || [];
|
||||
|
||||
const ordered = orderBy(this.messages, [(m) => {
|
||||
return m.timestamp;
|
||||
}], ['asc']);
|
||||
|
||||
if (subScribeToUpdate) {
|
||||
return ordered;
|
||||
}
|
||||
},
|
||||
filtersConversations () {
|
||||
if (!this.search) return this.conversations;
|
||||
return filter(this.conversations, (conversation) => {
|
||||
// Vue-subscribe to changes
|
||||
const subScribeToUpdate = this.updateConversionsCounter > -1;
|
||||
|
||||
const filtered = subScribeToUpdate && !this.search ?
|
||||
this.conversations :
|
||||
filter(this.conversations, (conversation) => {
|
||||
return conversation.name.toLowerCase().indexOf(this.search.toLowerCase()) !== -1;
|
||||
});
|
||||
|
||||
const ordered = orderBy(filtered, [(o) => {
|
||||
return moment(o.date).toDate();
|
||||
}], ['desc']);
|
||||
|
||||
return ordered;
|
||||
},
|
||||
currentLength () {
|
||||
return this.newMessage.length;
|
||||
@@ -424,25 +434,34 @@ export default {
|
||||
methods: {
|
||||
async onModalShown () {
|
||||
this.loaded = false;
|
||||
const res = await axios.get('/api/v4/inbox/messages');
|
||||
this.messages = res.data.data;
|
||||
|
||||
const conversationRes = await axios.get('/api/v4/inbox/conversations');
|
||||
this.loadedConversations = conversationRes.data.data;
|
||||
|
||||
this.loaded = true;
|
||||
},
|
||||
onModalHide () {
|
||||
this.messages = [];
|
||||
// reset everything
|
||||
this.loadedConversations = [];
|
||||
this.loaded = false;
|
||||
this.initiatedConversation = null;
|
||||
this.messagesByConversation = {};
|
||||
this.selectedConversation = {};
|
||||
},
|
||||
messageRemoved (message) {
|
||||
const messageIndex = this.messages.findIndex(msg => msg.id === message.id);
|
||||
if (messageIndex !== -1) this.messages.splice(messageIndex, 1);
|
||||
if (this.selectedConversationMessages.length === 0) this.initiatedConversation = {
|
||||
const messages = this.messagesByConversation[this.selectedConversation.key];
|
||||
|
||||
const messageIndex = messages.findIndex(msg => msg.id === message.id);
|
||||
if (messageIndex !== -1) messages.splice(messageIndex, 1);
|
||||
if (this.selectedConversationMessages.length === 0) {
|
||||
this.initiatedConversation = {
|
||||
uuid: this.selectedConversation.key,
|
||||
user: this.selectedConversation.name,
|
||||
username: this.selectedConversation.username,
|
||||
backer: this.selectedConversation.backer,
|
||||
contributor: this.selectedConversation.contributor,
|
||||
};
|
||||
}
|
||||
},
|
||||
toggleClick () {
|
||||
this.displayCreate = !this.displayCreate;
|
||||
@@ -450,12 +469,17 @@ export default {
|
||||
toggleOpt () {
|
||||
this.$store.dispatch('user:togglePrivateMessagesOpt');
|
||||
},
|
||||
selectConversation (key) {
|
||||
async selectConversation (key) {
|
||||
let convoFound = this.conversations.find((conversation) => {
|
||||
return conversation.key === key;
|
||||
});
|
||||
|
||||
this.selectedConversation = convoFound || {};
|
||||
this.page = 0;
|
||||
|
||||
if (!this.messagesByConversation[this.selectedConversation.key]) {
|
||||
await this.loadMessages();
|
||||
}
|
||||
|
||||
Vue.nextTick(() => {
|
||||
if (!this.$refs.chatscroll) return;
|
||||
@@ -466,18 +490,31 @@ export default {
|
||||
sendPrivateMessage () {
|
||||
if (!this.newMessage) return;
|
||||
|
||||
this.messages.push({
|
||||
const messages = this.messagesByConversation[this.selectedConversation.key];
|
||||
|
||||
messages.push({
|
||||
sent: true,
|
||||
text: this.newMessage,
|
||||
timestamp: new Date(),
|
||||
user: this.selectedConversation.name,
|
||||
username: this.selectedConversation.username,
|
||||
uuid: this.selectedConversation.key,
|
||||
toUser: this.selectedConversation.name,
|
||||
toUserName: this.selectedConversation.username,
|
||||
toUserContributor: this.selectedConversation.contributor,
|
||||
toUserBacker: this.selectedConversation.backer,
|
||||
toUUID: this.selectedConversation.uuid,
|
||||
|
||||
id: '-1', // will be updated once the result is back
|
||||
likes: {},
|
||||
ownerId: this.user._id,
|
||||
uuid: this.user._id,
|
||||
user: this.user.profile.name,
|
||||
username: this.user.auth.local.username,
|
||||
contributor: this.user.contributor,
|
||||
backer: this.user.backer,
|
||||
});
|
||||
|
||||
// Remove the placeholder message
|
||||
if (this.initiatedConversation && this.initiatedConversation.uuid === this.selectedConversation.key) {
|
||||
this.loadedConversations.unshift(this.initiatedConversation);
|
||||
this.initiatedConversation = null;
|
||||
}
|
||||
|
||||
@@ -495,7 +532,10 @@ export default {
|
||||
message: this.newMessage,
|
||||
}).then(response => {
|
||||
const newMessage = response.data.data.message;
|
||||
Object.assign(this.messages[this.messages.length - 1], newMessage);
|
||||
const messageToReset = messages[messages.length - 1];
|
||||
messageToReset.id = newMessage.id; // just set the id, all other infos already set
|
||||
Object.assign(messages[messages.length - 1], messageToReset);
|
||||
this.updateConversionsCounter++;
|
||||
});
|
||||
|
||||
this.newMessage = '';
|
||||
@@ -511,6 +551,34 @@ export default {
|
||||
if (!message.contributor) return;
|
||||
return this.icons[`tier${message.contributor.level}`];
|
||||
},
|
||||
infiniteScrollTrigger () {
|
||||
// show loading and wait until the loadMore debounced
|
||||
// or else it would trigger on every scrolling-pixel (while not loading)
|
||||
if (this.canLoadMore) {
|
||||
this.messagesLoading = true;
|
||||
}
|
||||
|
||||
return this.loadMore();
|
||||
},
|
||||
loadMore () {
|
||||
this.page += 1;
|
||||
return this.loadMessages();
|
||||
},
|
||||
async loadMessages () {
|
||||
this.messagesLoading = true;
|
||||
|
||||
const requestUrl = `/api/v4/inbox/messages?conversation=${this.selectedConversation.key}&page=${this.page}`;
|
||||
const res = await axios.get(requestUrl);
|
||||
const loadedMessages = res.data.data;
|
||||
|
||||
this.messagesByConversation[this.selectedConversation.key] = this.messagesByConversation[this.selectedConversation.key] || [];
|
||||
const loadedMessagesToAdd = loadedMessages.filter(m => this.messagesByConversation[this.selectedConversation.key].findIndex(mI => mI.id === m.id) === -1);
|
||||
this.messagesByConversation[this.selectedConversation.key].push(...loadedMessagesToAdd);
|
||||
|
||||
// only show the load more Button if the max count was returned
|
||||
this.canLoadMore = loadedMessages.length === 10;
|
||||
this.messagesLoading = false;
|
||||
},
|
||||
removeTags (html) {
|
||||
let tmp = document.createElement('DIV');
|
||||
tmp.innerHTML = html;
|
||||
|
||||
@@ -298,5 +298,6 @@
|
||||
"selected": "Selected",
|
||||
"howManyToBuy": "How many would you like to buy?",
|
||||
"habiticaHasUpdated": "There is a new Habitica update. Refresh to get the latest version!",
|
||||
"contactForm": "Contact the Moderation Team"
|
||||
"contactForm": "Contact the Moderation Team",
|
||||
"loadEarlierMessages": "Load Earlier Messages"
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '../../libs/email';
|
||||
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
||||
import { achievements } from '../../../../website/common/';
|
||||
import {sentMessage} from '../../libs/inbox';
|
||||
|
||||
let api = {};
|
||||
|
||||
@@ -633,6 +634,7 @@ api.sendPrivateMessage = {
|
||||
|
||||
const sender = res.locals.user;
|
||||
const message = req.body.message;
|
||||
|
||||
const receiver = await User.findById(req.body.toUserId).exec();
|
||||
if (!receiver) throw new NotFound(res.t('userNotFound'));
|
||||
if (!receiver.flags.verifiedUsername) delete receiver.auth.local.username;
|
||||
@@ -640,26 +642,7 @@ api.sendPrivateMessage = {
|
||||
const objections = sender.getObjectionsToInteraction('send-private-message', receiver);
|
||||
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
|
||||
|
||||
const messageSent = await sender.sendMessage(receiver, { receiverMsg: message });
|
||||
|
||||
if (receiver.preferences.emailNotifications.newPM !== false) {
|
||||
sendTxnEmail(receiver, 'new-pm', [
|
||||
{name: 'SENDER', content: getUserInfo(sender, ['name']).name},
|
||||
]);
|
||||
}
|
||||
|
||||
if (receiver.preferences.pushNotifications.newPM !== false) {
|
||||
sendPushNotification(
|
||||
receiver,
|
||||
{
|
||||
title: res.t('newPM'),
|
||||
message: res.t('newPMInfo', {name: getUserInfo(sender, ['name']).name, message}),
|
||||
identifier: 'newPM',
|
||||
category: 'newPM',
|
||||
payload: {replyTo: sender._id},
|
||||
}
|
||||
);
|
||||
}
|
||||
const messageSent = await sentMessage(sender, receiver, message, res.t);
|
||||
|
||||
res.respond(200, {message: messageSent});
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@ api.clearMessages = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /inbox/conversations Get the conversations for a user
|
||||
* @api {get} /api/v4/inbox/conversations Get the conversations for a user
|
||||
* @apiName conversations
|
||||
* @apiGroup Inbox
|
||||
* @apiDescription Get the conversations for a user
|
||||
@@ -93,4 +93,49 @@ api.conversations = {
|
||||
},
|
||||
};
|
||||
|
||||
function mapMessage (newChat, user) {
|
||||
if (newChat.sent) {
|
||||
newChat.toUUID = newChat.uuid;
|
||||
newChat.toUser = newChat.user;
|
||||
newChat.toUserName = newChat.username;
|
||||
newChat.toUserContributor = newChat.contributor;
|
||||
newChat.toUserBacker = newChat.backer;
|
||||
newChat.uuid = user._id;
|
||||
newChat.user = user.profile.name;
|
||||
newChat.username = user.auth.local.username;
|
||||
newChat.contributor = user.contributor;
|
||||
newChat.backer = user.backer;
|
||||
}
|
||||
|
||||
return newChat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /api/v4/inbox/messages Get inbox messages for a user
|
||||
* @apiName GetInboxMessages
|
||||
* @apiGroup Inbox
|
||||
* @apiDescription Get inbox messages for a user. Entries already populated with the correct `sent` - information
|
||||
*
|
||||
* @apiParam (Query) {Number} page Load the messages of the selected Page - 10 Messages per Page
|
||||
* @apiParam (Query) {GUID} conversation Loads only the messages of a conversation
|
||||
*
|
||||
* @apiSuccess {Array} data An array of inbox messages
|
||||
*/
|
||||
api.getInboxMessages = {
|
||||
method: 'GET',
|
||||
url: '/inbox/messages',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const user = res.locals.user;
|
||||
const page = req.query.page;
|
||||
const conversation = req.query.conversation;
|
||||
|
||||
const userInbox = (await inboxLib.getUserInbox(user, {
|
||||
page, conversation,
|
||||
})).map(newChat => mapMessage(newChat, user));
|
||||
|
||||
res.respond(200, userInbox);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
import {inboxModel as Inbox} from '../../models/message';
|
||||
import {
|
||||
model as User,
|
||||
} from '../../models/user';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import {getUserInfo, sendTxn as sendTxnEmail} from '../email';
|
||||
import {sendNotification as sendPushNotification} from '../pushNotifications';
|
||||
|
||||
const PM_PER_PAGE = 10;
|
||||
|
||||
export async function sentMessage (sender, receiver, message, translate) {
|
||||
const messageSent = await sender.sendMessage(receiver, { receiverMsg: message });
|
||||
|
||||
if (receiver.preferences.emailNotifications.newPM !== false) {
|
||||
sendTxnEmail(receiver, 'new-pm', [
|
||||
{name: 'SENDER', content: getUserInfo(sender, ['name']).name},
|
||||
]);
|
||||
}
|
||||
|
||||
if (receiver.preferences.pushNotifications.newPM !== false) {
|
||||
sendPushNotification(
|
||||
receiver,
|
||||
{
|
||||
title: translate('newPM'),
|
||||
message: translate('newPMInfo', {name: getUserInfo(sender, ['name']).name, message}),
|
||||
identifier: 'newPM',
|
||||
category: 'newPM',
|
||||
payload: {replyTo: sender._id},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return messageSent;
|
||||
}
|
||||
|
||||
export async function getUserInbox (user, options = {asArray: true, page: 0, conversation: null}) {
|
||||
if (typeof options.asArray === 'undefined') {
|
||||
options.asArray = true;
|
||||
@@ -40,34 +63,31 @@ export async function getUserInbox (user, options = {asArray: true, page: 0, con
|
||||
}
|
||||
}
|
||||
|
||||
export async function listConversations (user) {
|
||||
export async function listConversations (owner) {
|
||||
let query = Inbox
|
||||
.aggregate([
|
||||
{
|
||||
$match: {
|
||||
ownerId: user._id,
|
||||
ownerId: owner._id,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$uuid',
|
||||
user: {$first: '$user' },
|
||||
username: {$first: '$username' },
|
||||
timestamp: {$max: '$timestamp'}, // sort before group doesn't work - use the max value to sort it again after
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const conversationsList = orderBy(await query.exec(), ['timestamp'], ['desc']).map(c => c._id);
|
||||
const conversationsList = orderBy(await query.exec(), ['timestamp'], ['desc']);
|
||||
|
||||
const users = await User.find({_id: {$in: conversationsList}})
|
||||
.select('_id profile.name auth.local.username')
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
const usersMap = keyBy(users, '_id');
|
||||
const conversations = conversationsList.map(userId => ({
|
||||
uuid: usersMap[userId]._id,
|
||||
user: usersMap[userId].profile.name,
|
||||
username: usersMap[userId].auth.local.username,
|
||||
const conversations = conversationsList.map(({_id, user, username, timestamp}) => ({
|
||||
uuid: _id,
|
||||
user,
|
||||
username,
|
||||
timestamp,
|
||||
}));
|
||||
|
||||
return conversations;
|
||||
|
||||
@@ -44,6 +44,7 @@ const v4RouterOverrides = [
|
||||
'DELETE-/user/messages/:id',
|
||||
'DELETE-/user/messages',
|
||||
'POST-/coupons/enter/:code',
|
||||
'GET-/inbox/messages',
|
||||
];
|
||||
|
||||
const v4Router = express.Router(); // eslint-disable-line new-cap
|
||||
|
||||
@@ -98,8 +98,14 @@ export function setUserStyles (newMessage, user) {
|
||||
}
|
||||
}
|
||||
|
||||
let contributorCopy = user.contributor;
|
||||
if (contributorCopy && contributorCopy.toObject) {
|
||||
contributorCopy = contributorCopy.toObject();
|
||||
}
|
||||
|
||||
newMessage.contributor = contributorCopy;
|
||||
newMessage.userStyles = userStyles;
|
||||
newMessage.markModified('userStyles');
|
||||
newMessage.markModified('userStyles contributor');
|
||||
}
|
||||
|
||||
export function messageDefaults (msg, user, client, info = {}) {
|
||||
|
||||
Reference in New Issue
Block a user