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:
negue
2019-06-13 15:18:50 +02:00
committed by Matteo Pagliazzi
parent 5268bbb8a9
commit 5630e8cc8e
12 changed files with 355 additions and 115 deletions

View File

@@ -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', {

View File

@@ -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
});
});

View File

@@ -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;

View File

@@ -273,5 +273,8 @@ export default {
return habiticaMarkdown.render(String(text));
},
},
mounted () {
this.$emit('item-mounted', this.msg.id);
},
};
</script>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"
}

View File

@@ -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});
},

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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 = {}) {