diff --git a/test/api/v3/integration/inbox/GET-inbox_messages.test.js b/test/api/v3/integration/inbox/GET-inbox_messages.test.js
index e3436a3cc4..85b223bcab 100644
--- a/test/api/v3/integration/inbox/GET-inbox_messages.test.js
+++ b/test/api/v3/integration/inbox/GET-inbox_messages.test.js
@@ -47,7 +47,7 @@ describe('GET /inbox/messages', () => {
it('returns four messages when using page-query ', async () => {
const promises = [];
- for (let i = 0; i < 10; i += 1) {
+ for (let i = 0; i < 50; i += 1) {
promises.push(user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
diff --git a/test/api/v4/inbox/GET-inbox-conversations.test.js b/test/api/v4/inbox/GET-inbox-conversations.test.js
index 1ee66dda51..2803bd0fbb 100644
--- a/test/api/v4/inbox/GET-inbox-conversations.test.js
+++ b/test/api/v4/inbox/GET-inbox-conversations.test.js
@@ -66,7 +66,7 @@ describe('GET /inbox/conversations', () => {
it('returns five messages when using page-query ', async () => {
const promises = [];
- for (let i = 0; i < 10; i += 1) {
+ for (let i = 0; i < 50; i += 1) {
promises.push(user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
diff --git a/website/client/src/components/appFooter.vue b/website/client/src/components/appFooter.vue
index 39d02c94a8..0eae4cbf8f 100644
--- a/website/client/src/components/appFooter.vue
+++ b/website/client/src/components/appFooter.vue
@@ -396,6 +396,32 @@
class="btn btn-secondary"
@click="makeAdmin()"
>Make Admin
+
+
@@ -886,6 +912,8 @@ export default {
DEBUG_ENABLED,
TIME_TRAVEL_ENABLED,
lastTimeJump: null,
+ partyChatCount: 450,
+ inboxCount: 450,
};
},
computed: {
@@ -1004,6 +1032,32 @@ export default {
// Reload the website then go to Help > Admin Panel to set contributor level, etc.');
// @TODO: sync()
},
+ async seedPartyChat () {
+ try {
+ const count = this.partyChatCount;
+ if (!Number.isInteger(count) || count < 1) {
+ window.alert('Please enter a positive integer'); // eslint-disable-line no-alert
+ return;
+ }
+ await axios.post('/api/v4/debug/seed-party-chat', { messageCount: count });
+ window.alert(`Successfully sent ${count} messages to your party chat!`); // eslint-disable-line no-alert
+ } catch (e) {
+ window.alert(e.response?.data?.message || 'Error sending party chat messages'); // eslint-disable-line no-alert
+ }
+ },
+ async seedInbox () {
+ try {
+ const count = this.inboxCount;
+ if (!Number.isInteger(count) || count < 1) {
+ window.alert('Please enter a positive integer'); // eslint-disable-line no-alert
+ return;
+ }
+ await axios.post('/api/v4/debug/seed-inbox', { messageCount: count });
+ window.alert(`Successfully sent ${count} messages to your inbox!`); // eslint-disable-line no-alert
+ } catch (e) {
+ window.alert(e.response?.data?.message || 'Error sending inbox messages'); // eslint-disable-line no-alert
+ }
+ },
donate () {
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
},
diff --git a/website/client/src/pages/private-messages/index.vue b/website/client/src/pages/private-messages/index.vue
index 4e051a3b00..4e347ce870 100644
--- a/website/client/src/pages/private-messages/index.vue
+++ b/website/client/src/pages/private-messages/index.vue
@@ -679,7 +679,7 @@ import NotificationMixins from '@/mixins/notifications';
// extract to a shared path
const CONVERSATIONS_PER_PAGE = 10;
-const PM_PER_PAGE = 10;
+const PM_PER_PAGE = 50;
const UI_STATES = Object.freeze({
LOADING: 'LOADING',
diff --git a/website/client/src/store/actions/chat.js b/website/client/src/store/actions/chat.js
index 855eba8696..836e6d0f7c 100644
--- a/website/client/src/store/actions/chat.js
+++ b/website/client/src/store/actions/chat.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import * as Analytics from '@/libs/analytics';
export async function getChat (store, payload) {
- const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat`);
+ const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`);
return response.data.data;
}
diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js
index 65dce42abe..32ac046a70 100644
--- a/website/server/controllers/api-v3/chat.js
+++ b/website/server/controllers/api-v3/chat.js
@@ -64,6 +64,8 @@ function textContainsBannedSlur (message) {
*
* @apiParam (Path) {String} groupId The group _id ('party' for the user party and
* 'habitrpg' for tavern are accepted).
+ * @apiParam (Query) {Number} [limit=50] The number of messages to fetch (max 400).
+ * @apiParam (Query) {String} [before] Fetch messages older than this message ID.
*
* @apiSuccess {Array} data An array of chat messages
*
@@ -78,18 +80,21 @@ api.getChat = {
const { user } = res.locals;
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
+ req.checkQuery('before').optional().isUUID();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
const { groupId } = req.params;
+ const limit = req.query.limit ? Math.min(parseInt(req.query.limit, 10), 400) : 50;
+ const { before } = req.query;
const group = await Group.getGroup({ user, groupId, fields: 'chat privacy' });
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.privacy === 'public') {
throw new BadRequest(res.t('featureRetired'));
}
- const groupChat = await Group.toJSONCleanChat(group, user);
+ const groupChat = await Group.toJSONCleanChat(group, user, { limit, before });
res.respond(200, groupChat.chat);
},
};
diff --git a/website/server/controllers/api-v3/debug.js b/website/server/controllers/api-v3/debug.js
index 108cf2dbaa..d70eeeeb86 100644
--- a/website/server/controllers/api-v3/debug.js
+++ b/website/server/controllers/api-v3/debug.js
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
import get from 'lodash/get';
import sinon from 'sinon';
import moment from 'moment';
+import { v4 as uuid } from 'uuid';
import { authWithHeaders } from '../../middlewares/auth';
import ensureDevelopmentMode from '../../middlewares/ensureDevelopmentMode';
import ensureTimeTravelMode from '../../middlewares/ensureTimeTravelMode';
@@ -11,6 +12,7 @@ import {
model as Group,
// basicFields as basicGroupFields,
} from '../../models/group';
+import { chatModel as Chat, inboxModel as Inbox } from '../../models/message';
import connectToMongoDB from '../../libs/mongoose';
const { content } = common;
@@ -311,4 +313,93 @@ api.timeTravelAdjust = {
},
};
+api.seedPartyChat = {
+ method: 'POST',
+ url: '/debug/seed-party-chat',
+ middlewares: [ensureDevelopmentMode, authWithHeaders()],
+ async handler (req, res) {
+ const { user } = res.locals;
+ const messageCount = Number(req.body.messageCount);
+
+ if (!Number.isInteger(messageCount) || messageCount < 1) {
+ throw new BadRequest('messageCount must be a positive integer.');
+ }
+
+ if (!user.party._id) {
+ throw new BadRequest('You are not in a party.');
+ }
+
+ const party = await Group.findOne({ _id: user.party._id, type: 'party' }).exec();
+ if (!party) {
+ throw new BadRequest('Party not found.');
+ }
+
+ const messages = [];
+ const baseTimestamp = Date.now();
+
+ for (let i = 1; i <= messageCount; i += 1) {
+ const id = uuid();
+ messages.push({
+ _id: id,
+ id,
+ groupId: party._id,
+ text: `#${i}`,
+ unformattedText: `#${i}`,
+ timestamp: new Date(baseTimestamp - (messageCount - i) * 1000),
+ likes: {},
+ flags: {},
+ flagCount: 0,
+ uuid: 'system',
+ user: 'System',
+ client: 'debug-seed',
+ });
+ }
+
+ await Chat.insertMany(messages);
+
+ res.respond(200, { messageCount });
+ },
+};
+
+// Messaging ourselves for testing
+api.seedInbox = {
+ method: 'POST',
+ url: '/debug/seed-inbox',
+ middlewares: [ensureDevelopmentMode, authWithHeaders()],
+ async handler (req, res) {
+ const { user } = res.locals;
+ const messageCount = Number(req.body.messageCount);
+
+ if (!Number.isInteger(messageCount) || messageCount < 1) {
+ throw new BadRequest('messageCount must be a positive integer.');
+ }
+
+ const messages = [];
+ const baseTimestamp = Date.now();
+
+ for (let i = 1; i <= messageCount; i += 1) {
+ const id = uuid();
+ messages.push({
+ _id: id,
+ id,
+ ownerId: user._id,
+ uuid: user._id,
+ user: user.profile.name,
+ text: `#${i}`,
+ unformattedText: `#${i}`,
+ timestamp: new Date(baseTimestamp - (messageCount - i) * 1000),
+ likes: {},
+ flags: {},
+ flagCount: 0,
+ sent: true,
+ client: 'debug-seed',
+ });
+ }
+
+ await Inbox.insertMany(messages);
+
+ res.respond(200, { messageCount });
+ },
+};
+
export default api;
diff --git a/website/server/libs/chat/group-chat.js b/website/server/libs/chat/group-chat.js
index 445c72528d..028f987b4a 100644
--- a/website/server/libs/chat/group-chat.js
+++ b/website/server/libs/chat/group-chat.js
@@ -9,7 +9,9 @@ import { // eslint-disable-line import/no-cycle
const questScrolls = shared.content.quests;
// @TODO: Don't use this method when the group can be saved.
-export async function getGroupChat (group) {
+export async function getGroupChat (group, options = {}) {
+ const { limit, before } = options;
+
let maxChatCount = MAX_CHAT_COUNT;
if (group.chatLimitCount && group.chatLimitCount >= MAX_CHAT_COUNT) {
maxChatCount = group.chatLimitCount;
@@ -17,10 +19,19 @@ export async function getGroupChat (group) {
maxChatCount = MAX_SUBBED_GROUP_CHAT_COUNT;
}
- const groupChat = await Chat.find({ groupId: group._id })
- .limit(maxChatCount)
- .sort('-timestamp')
- .exec();
+ const effectiveLimit = limit !== undefined ? Math.min(limit, maxChatCount) : maxChatCount;
+
+ let query = Chat.find({ groupId: group._id })
+ .sort('-timestamp');
+
+ if (before) {
+ const beforeMessage = await Chat.findOne({ _id: before }).exec();
+ if (beforeMessage) {
+ query = query.where('timestamp').lt(beforeMessage.timestamp);
+ }
+ }
+
+ const groupChat = await query.limit(effectiveLimit).exec();
// @TODO: Concat old chat to keep continuity of chat stored on group object
const currentGroupChat = group.chat || [];
diff --git a/website/server/libs/inbox/index.js b/website/server/libs/inbox/index.js
index 3cc2c2228b..af0d7ae07d 100644
--- a/website/server/libs/inbox/index.js
+++ b/website/server/libs/inbox/index.js
@@ -37,7 +37,9 @@ export async function sentMessage (sender, receiver, message, translate) {
return messageSent;
}
-const PM_PER_PAGE = 10;
+// Paginate per every 50
+const PM_PER_PAGE = 50;
+const MAX_PM_COUNT = 400;
const getUserInboxDefaultOptions = {
asArray: true,
@@ -61,12 +63,18 @@ export async function getUserInbox (user, optionParams = getUserInboxDefaultOpti
.sort({ timestamp: -1 });
if (typeof options.page !== 'undefined') {
+ const page = Number(options.page);
+ const skip = PM_PER_PAGE * page;
+ if (skip >= MAX_PM_COUNT) {
+ return options.asArray ? [] : {};
+ }
+ const remainingAllowed = MAX_PM_COUNT - skip;
+ const limit = Math.min(PM_PER_PAGE, remainingAllowed);
query = query
- .skip(PM_PER_PAGE * Number(options.page))
- .limit(PM_PER_PAGE);
+ .skip(skip)
+ .limit(limit);
} else {
- // Limit for legacy calls that are not paginated to prevent database issues
- query = query.limit(200);
+ query = query.limit(MAX_PM_COUNT);
}
const messages = (await query.lean().exec()).map(msgObj => {
diff --git a/website/server/models/group.js b/website/server/models/group.js
index 7d181c818e..36c678c2cd 100644
--- a/website/server/models/group.js
+++ b/website/server/models/group.js
@@ -345,12 +345,12 @@ schema.statics.getGroups = async function getGroups (options = {}) {
// unless the user is an admin or said chat is posted by that user
// Not putting into toJSON because there we can't access user
// It also removes the _meta field that can be stored inside a chat message
-schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, user) {
+schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, user, options = {}) {
// @TODO: Adding this here for support the old chat,
// but we should depreciate accessing chat like this
// Also only return chat if requested, eventually we don't want to return chat here
if (group && group.chat) {
- await getGroupChat(group);
+ await getGroupChat(group, options);
}
const groupToJson = group.toJSON();