Skip to content

Commit 2917955

Browse files
HafizzleSabreCat
andauthored
Chat optimization (#15545)
* fix(content): textual tweaks and updates * fix(link): direct to FAQ instead of wiki * fix(faq): correct Markdown * Show orb of rebirth confirmation modal after use (window refresh) * Set and check rebirth confirmation modal from localstorage Set and check rebirth confirmation modal from localstorage after window reload * Don't show orb of rebirth confirmation modal until page reloads * message effective limit optimization * Keep max limit for web (400 recent messages) * Fix amount of messages initially being shown * PM_PER_PAGE set to 50 * Increases number of messages in inbox test * Increases number of messages for inbox pagination test * Set and check rebirth confirmation modal from localstorage Set and check rebirth confirmation modal from localstorage after window reload * Don't show orb of rebirth confirmation modal until page reloads * message effective limit optimization * Keep max limit for web (400 recent messages) * Add UUID validation for 'before' query parameter * add party message stress test tool in admin panel * lint * add MAX_PM_COUNT of 400, admin tool for stress testing messages * comment * update stress test inbox message tool to use logged in user * comment --------- Co-authored-by: Kalista Payne <[email protected]>
1 parent 55d13e4 commit 2917955

File tree

10 files changed

+186
-17
lines changed

10 files changed

+186
-17
lines changed

test/api/v3/integration/inbox/GET-inbox_messages.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('GET /inbox/messages', () => {
4747
it('returns four messages when using page-query ', async () => {
4848
const promises = [];
4949

50-
for (let i = 0; i < 10; i += 1) {
50+
for (let i = 0; i < 50; i += 1) {
5151
promises.push(user.post('/members/send-private-message', {
5252
toUserId: user.id,
5353
message: 'fourth',

test/api/v4/inbox/GET-inbox-conversations.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('GET /inbox/conversations', () => {
6666
it('returns five messages when using page-query ', async () => {
6767
const promises = [];
6868

69-
for (let i = 0; i < 10; i += 1) {
69+
for (let i = 0; i < 50; i += 1) {
7070
promises.push(user.post('/members/send-private-message', {
7171
toUserId: user.id,
7272
message: 'fourth',

website/client/src/components/appFooter.vue

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,32 @@
396396
class="btn btn-secondary"
397397
@click="makeAdmin()"
398398
>Make Admin</a>
399+
<div class="d-flex align-items-center mt-2">
400+
<input
401+
v-model.number="partyChatCount"
402+
type="number"
403+
min="1"
404+
class="form-control form-control-sm mr-2"
405+
style="width: 80px;"
406+
>
407+
<a
408+
class="btn btn-secondary"
409+
@click="seedPartyChat()"
410+
>Send Party Chat Messages</a>
411+
</div>
412+
<div class="d-flex align-items-center mt-2">
413+
<input
414+
v-model.number="inboxCount"
415+
type="number"
416+
min="1"
417+
class="form-control form-control-sm mr-2"
418+
style="width: 80px;"
419+
>
420+
<a
421+
class="btn btn-secondary"
422+
@click="seedInbox()"
423+
>Send Inbox Messages</a>
424+
</div>
399425
</div>
400426
</div>
401427
</div>
@@ -886,6 +912,8 @@ export default {
886912
DEBUG_ENABLED,
887913
TIME_TRAVEL_ENABLED,
888914
lastTimeJump: null,
915+
partyChatCount: 450,
916+
inboxCount: 450,
889917
};
890918
},
891919
computed: {
@@ -1004,6 +1032,32 @@ export default {
10041032
// Reload the website then go to Help > Admin Panel to set contributor level, etc.');
10051033
// @TODO: sync()
10061034
},
1035+
async seedPartyChat () {
1036+
try {
1037+
const count = this.partyChatCount;
1038+
if (!Number.isInteger(count) || count < 1) {
1039+
window.alert('Please enter a positive integer'); // eslint-disable-line no-alert
1040+
return;
1041+
}
1042+
await axios.post('/api/v4/debug/seed-party-chat', { messageCount: count });
1043+
window.alert(`Successfully sent ${count} messages to your party chat!`); // eslint-disable-line no-alert
1044+
} catch (e) {
1045+
window.alert(e.response?.data?.message || 'Error sending party chat messages'); // eslint-disable-line no-alert
1046+
}
1047+
},
1048+
async seedInbox () {
1049+
try {
1050+
const count = this.inboxCount;
1051+
if (!Number.isInteger(count) || count < 1) {
1052+
window.alert('Please enter a positive integer'); // eslint-disable-line no-alert
1053+
return;
1054+
}
1055+
await axios.post('/api/v4/debug/seed-inbox', { messageCount: count });
1056+
window.alert(`Successfully sent ${count} messages to your inbox!`); // eslint-disable-line no-alert
1057+
} catch (e) {
1058+
window.alert(e.response?.data?.message || 'Error sending inbox messages'); // eslint-disable-line no-alert
1059+
}
1060+
},
10071061
donate () {
10081062
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
10091063
},

website/client/src/pages/private-messages/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ import NotificationMixins from '@/mixins/notifications';
679679
680680
// extract to a shared path
681681
const CONVERSATIONS_PER_PAGE = 10;
682-
const PM_PER_PAGE = 10;
682+
const PM_PER_PAGE = 50;
683683
684684
const UI_STATES = Object.freeze({
685685
LOADING: 'LOADING',

website/client/src/store/actions/chat.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Vue from 'vue';
33
import * as Analytics from '@/libs/analytics';
44

55
export async function getChat (store, payload) {
6-
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat`);
6+
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`);
77

88
return response.data.data;
99
}

website/server/controllers/api-v3/chat.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ function textContainsBannedSlur (message) {
6464
*
6565
* @apiParam (Path) {String} groupId The group _id ('party' for the user party and
6666
* 'habitrpg' for tavern are accepted).
67+
* @apiParam (Query) {Number} [limit=50] The number of messages to fetch (max 400).
68+
* @apiParam (Query) {String} [before] Fetch messages older than this message ID.
6769
*
6870
* @apiSuccess {Array} data An array of <a href='https://github.com/HabitRPG/habitica/blob/develop/website/server/models/group.js#L51' target='_blank'>chat messages</a>
6971
*
@@ -78,18 +80,21 @@ api.getChat = {
7880
const { user } = res.locals;
7981

8082
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
83+
req.checkQuery('before').optional().isUUID();
8184

8285
const validationErrors = req.validationErrors();
8386
if (validationErrors) throw validationErrors;
8487

8588
const { groupId } = req.params;
89+
const limit = req.query.limit ? Math.min(parseInt(req.query.limit, 10), 400) : 50;
90+
const { before } = req.query;
8691
const group = await Group.getGroup({ user, groupId, fields: 'chat privacy' });
8792
if (!group) throw new NotFound(res.t('groupNotFound'));
8893
if (group.privacy === 'public') {
8994
throw new BadRequest(res.t('featureRetired'));
9095
}
9196

92-
const groupChat = await Group.toJSONCleanChat(group, user);
97+
const groupChat = await Group.toJSONCleanChat(group, user, { limit, before });
9398
res.respond(200, groupChat.chat);
9499
},
95100
};

website/server/controllers/api-v3/debug.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
22
import get from 'lodash/get';
33
import sinon from 'sinon';
44
import moment from 'moment';
5+
import { v4 as uuid } from 'uuid';
56
import { authWithHeaders } from '../../middlewares/auth';
67
import ensureDevelopmentMode from '../../middlewares/ensureDevelopmentMode';
78
import ensureTimeTravelMode from '../../middlewares/ensureTimeTravelMode';
@@ -11,6 +12,7 @@ import {
1112
model as Group,
1213
// basicFields as basicGroupFields,
1314
} from '../../models/group';
15+
import { chatModel as Chat, inboxModel as Inbox } from '../../models/message';
1416
import connectToMongoDB from '../../libs/mongoose';
1517

1618
const { content } = common;
@@ -311,4 +313,93 @@ api.timeTravelAdjust = {
311313
},
312314
};
313315

316+
api.seedPartyChat = {
317+
method: 'POST',
318+
url: '/debug/seed-party-chat',
319+
middlewares: [ensureDevelopmentMode, authWithHeaders()],
320+
async handler (req, res) {
321+
const { user } = res.locals;
322+
const messageCount = Number(req.body.messageCount);
323+
324+
if (!Number.isInteger(messageCount) || messageCount < 1) {
325+
throw new BadRequest('messageCount must be a positive integer.');
326+
}
327+
328+
if (!user.party._id) {
329+
throw new BadRequest('You are not in a party.');
330+
}
331+
332+
const party = await Group.findOne({ _id: user.party._id, type: 'party' }).exec();
333+
if (!party) {
334+
throw new BadRequest('Party not found.');
335+
}
336+
337+
const messages = [];
338+
const baseTimestamp = Date.now();
339+
340+
for (let i = 1; i <= messageCount; i += 1) {
341+
const id = uuid();
342+
messages.push({
343+
_id: id,
344+
id,
345+
groupId: party._id,
346+
text: `#${i}`,
347+
unformattedText: `#${i}`,
348+
timestamp: new Date(baseTimestamp - (messageCount - i) * 1000),
349+
likes: {},
350+
flags: {},
351+
flagCount: 0,
352+
uuid: 'system',
353+
user: 'System',
354+
client: 'debug-seed',
355+
});
356+
}
357+
358+
await Chat.insertMany(messages);
359+
360+
res.respond(200, { messageCount });
361+
},
362+
};
363+
364+
// Messaging ourselves for testing
365+
api.seedInbox = {
366+
method: 'POST',
367+
url: '/debug/seed-inbox',
368+
middlewares: [ensureDevelopmentMode, authWithHeaders()],
369+
async handler (req, res) {
370+
const { user } = res.locals;
371+
const messageCount = Number(req.body.messageCount);
372+
373+
if (!Number.isInteger(messageCount) || messageCount < 1) {
374+
throw new BadRequest('messageCount must be a positive integer.');
375+
}
376+
377+
const messages = [];
378+
const baseTimestamp = Date.now();
379+
380+
for (let i = 1; i <= messageCount; i += 1) {
381+
const id = uuid();
382+
messages.push({
383+
_id: id,
384+
id,
385+
ownerId: user._id,
386+
uuid: user._id,
387+
user: user.profile.name,
388+
text: `#${i}`,
389+
unformattedText: `#${i}`,
390+
timestamp: new Date(baseTimestamp - (messageCount - i) * 1000),
391+
likes: {},
392+
flags: {},
393+
flagCount: 0,
394+
sent: true,
395+
client: 'debug-seed',
396+
});
397+
}
398+
399+
await Inbox.insertMany(messages);
400+
401+
res.respond(200, { messageCount });
402+
},
403+
};
404+
314405
export default api;

website/server/libs/chat/group-chat.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,29 @@ import { // eslint-disable-line import/no-cycle
99
const questScrolls = shared.content.quests;
1010

1111
// @TODO: Don't use this method when the group can be saved.
12-
export async function getGroupChat (group) {
12+
export async function getGroupChat (group, options = {}) {
13+
const { limit, before } = options;
14+
1315
let maxChatCount = MAX_CHAT_COUNT;
1416
if (group.chatLimitCount && group.chatLimitCount >= MAX_CHAT_COUNT) {
1517
maxChatCount = group.chatLimitCount;
1618
} else if (group.hasActiveGroupPlan()) {
1719
maxChatCount = MAX_SUBBED_GROUP_CHAT_COUNT;
1820
}
1921

20-
const groupChat = await Chat.find({ groupId: group._id })
21-
.limit(maxChatCount)
22-
.sort('-timestamp')
23-
.exec();
22+
const effectiveLimit = limit !== undefined ? Math.min(limit, maxChatCount) : maxChatCount;
23+
24+
let query = Chat.find({ groupId: group._id })
25+
.sort('-timestamp');
26+
27+
if (before) {
28+
const beforeMessage = await Chat.findOne({ _id: before }).exec();
29+
if (beforeMessage) {
30+
query = query.where('timestamp').lt(beforeMessage.timestamp);
31+
}
32+
}
33+
34+
const groupChat = await query.limit(effectiveLimit).exec();
2435

2536
// @TODO: Concat old chat to keep continuity of chat stored on group object
2637
const currentGroupChat = group.chat || [];

website/server/libs/inbox/index.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ export async function sentMessage (sender, receiver, message, translate) {
3737

3838
return messageSent;
3939
}
40-
const PM_PER_PAGE = 10;
40+
// Paginate per every 50
41+
const PM_PER_PAGE = 50;
42+
const MAX_PM_COUNT = 400;
4143

4244
const getUserInboxDefaultOptions = {
4345
asArray: true,
@@ -61,12 +63,18 @@ export async function getUserInbox (user, optionParams = getUserInboxDefaultOpti
6163
.sort({ timestamp: -1 });
6264

6365
if (typeof options.page !== 'undefined') {
66+
const page = Number(options.page);
67+
const skip = PM_PER_PAGE * page;
68+
if (skip >= MAX_PM_COUNT) {
69+
return options.asArray ? [] : {};
70+
}
71+
const remainingAllowed = MAX_PM_COUNT - skip;
72+
const limit = Math.min(PM_PER_PAGE, remainingAllowed);
6473
query = query
65-
.skip(PM_PER_PAGE * Number(options.page))
66-
.limit(PM_PER_PAGE);
74+
.skip(skip)
75+
.limit(limit);
6776
} else {
68-
// Limit for legacy calls that are not paginated to prevent database issues
69-
query = query.limit(200);
77+
query = query.limit(MAX_PM_COUNT);
7078
}
7179

7280
const messages = (await query.lean().exec()).map(msgObj => {

website/server/models/group.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,12 @@ schema.statics.getGroups = async function getGroups (options = {}) {
345345
// unless the user is an admin or said chat is posted by that user
346346
// Not putting into toJSON because there we can't access user
347347
// It also removes the _meta field that can be stored inside a chat message
348-
schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, user) {
348+
schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, user, options = {}) {
349349
// @TODO: Adding this here for support the old chat,
350350
// but we should depreciate accessing chat like this
351351
// Also only return chat if requested, eventually we don't want to return chat here
352352
if (group && group.chat) {
353-
await getGroupChat(group);
353+
await getGroupChat(group, options);
354354
}
355355

356356
const groupToJson = group.toJSON();

0 commit comments

Comments
 (0)