nodebb-plugin-admin-chats 3.8.1 → 3.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/hooks.js +5 -1
- package/lib/overrides.js +41 -0
- package/lib/routes.js +63 -0
- package/library.js +2 -1
- package/package.json +1 -1
- package/plugin.json +2 -1
- package/static/lib/chat-search.js +80 -0
- package/static/lib/switch-chat.js +6 -0
package/lib/hooks.js
CHANGED
|
@@ -243,7 +243,11 @@ hooks.onLoadRoom = async function (payload) {
|
|
|
243
243
|
isLockedForUser
|
|
244
244
|
);
|
|
245
245
|
room.canReply = room.canReply && !isLockedForUser;
|
|
246
|
-
|
|
246
|
+
// The composer in partials/chats/message-window.tpl is wrapped in {{{ if showUserInput }}}.
|
|
247
|
+
// Core sets showUserInput = false whenever "maximum users in chat room" is 2 (the flag was
|
|
248
|
+
// meant for the add-user field and core templates no longer use it), which hid the message
|
|
249
|
+
// box on /chats - the page phones use. Only hide it when the room is locked for the viewer.
|
|
250
|
+
room.showUserInput = !isLockedForUser;
|
|
247
251
|
|
|
248
252
|
if (!hasAccess) {
|
|
249
253
|
return payload;
|
package/lib/overrides.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
const { isRoomLocked } = require('./lock');
|
|
17
17
|
const { editHistoryKey } = require('./history');
|
|
18
18
|
const privileges = nodebb.require('./src/privileges');
|
|
19
|
+
const helpers = nodebb.require('./src/controllers/helpers');
|
|
19
20
|
|
|
20
21
|
// NodeBB translates `[[namespace:key]]` error messages itself (both in
|
|
21
22
|
// app.alertError on the client and anywhere else the message surfaces) —
|
|
@@ -46,6 +47,45 @@ function overrideCoreChatRedirect(controllers, renderAdminChatsPage) {
|
|
|
46
47
|
chatsController.redirectToChat = wrappedRedirectToChat;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// /user/<slug>/chats/<roomId> renders the room as seen by <slug>: core loads
|
|
51
|
+
// its messages from that user's join timestamp, so for a private room the
|
|
52
|
+
// user is not in it comes out empty (title only). Send the viewer to a URL
|
|
53
|
+
// that can actually show the room — their own chats page if they are in it,
|
|
54
|
+
// otherwise the all-chats page.
|
|
55
|
+
function overrideCoreChatsPage(controllers) {
|
|
56
|
+
const chatsController = controllers && controllers.accounts && controllers.accounts.chats;
|
|
57
|
+
if (!chatsController || typeof chatsController.get !== 'function' || chatsController.get._adminChatsWrapped) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const originalGet = chatsController.get;
|
|
62
|
+
const wrappedGet = async function (req, res, next) {
|
|
63
|
+
const roomId = parseInt(req.params.roomid, 10);
|
|
64
|
+
const isSwitch = res.locals.isAPI && parseInt(req.query.switch, 10) === 1;
|
|
65
|
+
if (roomId && !isSwitch && req.uid && await canAccessAdminChats(req.uid)) {
|
|
66
|
+
const uid = await User.getUidByUserslug(req.params.userslug);
|
|
67
|
+
if (uid && parseInt(uid, 10) !== parseInt(req.uid, 10)) {
|
|
68
|
+
const [room, isOwnerInRoom, isCallerInRoom] = await Promise.all([
|
|
69
|
+
Messaging.getRoomData(roomId),
|
|
70
|
+
isUserMemberOfRoom(uid, roomId),
|
|
71
|
+
isUserMemberOfRoom(req.uid, roomId),
|
|
72
|
+
]);
|
|
73
|
+
if (room && !room.public && !isOwnerInRoom) {
|
|
74
|
+
const index = parseInt(req.params.index, 10);
|
|
75
|
+
const suffix = `${roomId}${index ? `/${index}` : ''}`;
|
|
76
|
+
const callerSlug = isCallerInRoom && await User.getUserField(req.uid, 'userslug');
|
|
77
|
+
return helpers.redirect(res, callerSlug ? `/user/${callerSlug}/chats/${suffix}` : `/chats/${suffix}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return await originalGet.call(this, req, res, next);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
wrappedGet._adminChatsWrapped = true;
|
|
86
|
+
chatsController.get = wrappedGet;
|
|
87
|
+
}
|
|
88
|
+
|
|
49
89
|
function overrideChatsApi() {
|
|
50
90
|
if (!ChatsAPI || typeof ChatsAPI.kick !== 'function' || ChatsAPI.kick._adminChatLockWrapped) {
|
|
51
91
|
return;
|
|
@@ -181,6 +221,7 @@ function overrideMessagingFunctions() {
|
|
|
181
221
|
module.exports = {
|
|
182
222
|
getMessageRoomId,
|
|
183
223
|
overrideCoreChatRedirect,
|
|
224
|
+
overrideCoreChatsPage,
|
|
184
225
|
overrideChatsApi,
|
|
185
226
|
overrideMessagingFunctions,
|
|
186
227
|
};
|
package/lib/routes.js
CHANGED
|
@@ -14,6 +14,7 @@ const { roomExists, lockRoom, unlockRoom, getRoomLockData } = require('./lock');
|
|
|
14
14
|
const { ACTIVITY_SET, pruneActivityEntries } = require('./activity');
|
|
15
15
|
|
|
16
16
|
const ADMIN_CHAT_PAGE_SIZE = 30;
|
|
17
|
+
const ADMIN_CHAT_SEARCH_LIMIT = 100;
|
|
17
18
|
|
|
18
19
|
async function assertAdminChatsAccess(req, res) {
|
|
19
20
|
if (!req.uid || !await canAccessAdminChats(req.uid)) {
|
|
@@ -202,6 +203,41 @@ async function enrichAdminRecentRooms(uid, roomIds, rooms) {
|
|
|
202
203
|
});
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
async function searchAdminChats(uid, query) {
|
|
207
|
+
const lowerQuery = String(query).toLowerCase();
|
|
208
|
+
const allRoomIds = await db.getSortedSetRevRange(ACTIVITY_SET, 0, -1);
|
|
209
|
+
const [allRooms, roomUids] = await Promise.all([
|
|
210
|
+
Messaging.getRoomsData(allRoomIds, ['roomName', 'public']),
|
|
211
|
+
db.getSortedSetsMembers(allRoomIds.map(roomId => `chat:room:${roomId}:uids`)),
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const uniqueUids = [...new Set(roomUids.flat().filter(Boolean))];
|
|
215
|
+
const users = await User.getUsersFields(uniqueUids, ['username', 'displayname']);
|
|
216
|
+
const matchingUids = new Set();
|
|
217
|
+
users.forEach((user, index) => {
|
|
218
|
+
if (user && (
|
|
219
|
+
(user.displayname && user.displayname.toLowerCase().includes(lowerQuery)) ||
|
|
220
|
+
(user.username && user.username.toLowerCase().includes(lowerQuery))
|
|
221
|
+
)) {
|
|
222
|
+
matchingUids.add(String(uniqueUids[index]));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const roomIds = allRoomIds.filter((roomId, index) => {
|
|
227
|
+
const room = allRooms[index];
|
|
228
|
+
if (!room || room.public) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return (room.roomName && room.roomName.toLowerCase().includes(lowerQuery)) ||
|
|
232
|
+
(roomUids[index] || []).some(memberUid => matchingUids.has(String(memberUid)));
|
|
233
|
+
}).slice(0, ADMIN_CHAT_SEARCH_LIMIT);
|
|
234
|
+
|
|
235
|
+
const rooms = await Messaging.getRoomsData(roomIds);
|
|
236
|
+
await enrichAdminRecentRooms(uid, roomIds, rooms);
|
|
237
|
+
|
|
238
|
+
return { rooms: rooms.filter(Boolean) };
|
|
239
|
+
}
|
|
240
|
+
|
|
205
241
|
function buildParticipantsLabel(users) {
|
|
206
242
|
const list = Array.isArray(users) ? users.filter(Boolean) : [];
|
|
207
243
|
if (!list.length) {
|
|
@@ -308,6 +344,33 @@ function registerRoutes(app, router, middleware) {
|
|
|
308
344
|
}
|
|
309
345
|
});
|
|
310
346
|
|
|
347
|
+
// Core's chat list search (GET /api/v3/chats/search) always searches the
|
|
348
|
+
// caller's own rooms, so on the all-chats page and on another user's chats
|
|
349
|
+
// page it would list the viewer's chats instead of the ones being viewed.
|
|
350
|
+
router.get('/api/admin-chats/search', ...routeMiddleware, async (req, res) => {
|
|
351
|
+
try {
|
|
352
|
+
if (!await assertAdminChatsAccess(req, res)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const query = String(req.query.query || '').trim();
|
|
357
|
+
const uid = parseInt(req.query.uid, 10) || 0;
|
|
358
|
+
let data;
|
|
359
|
+
if (uid) {
|
|
360
|
+
data = query ?
|
|
361
|
+
await Messaging.searchRecentChats(req.uid, uid, query) :
|
|
362
|
+
await Messaging.getRecentChats(req.uid, uid, 0, ADMIN_CHAT_PAGE_SIZE - 1);
|
|
363
|
+
} else {
|
|
364
|
+
data = query ?
|
|
365
|
+
await searchAdminChats(req.uid, query) :
|
|
366
|
+
await getAdminRecentChats(req.uid, 0, ADMIN_CHAT_PAGE_SIZE);
|
|
367
|
+
}
|
|
368
|
+
res.json(data);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
res.status(500).json({ status: { code: 'error', message: err.message } });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
311
374
|
router.get('/api/admin-chats/page/:roomId?/:index?', ...routeMiddleware, async (req, res) => {
|
|
312
375
|
try {
|
|
313
376
|
if (!await assertAdminChatsAccess(req, res)) {
|
package/library.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const hooks = require('./lib/hooks');
|
|
4
4
|
const { registerRoutes, renderAdminChatsPage } = require('./lib/routes');
|
|
5
|
-
const { overrideMessagingFunctions, overrideChatsApi, overrideCoreChatRedirect } = require('./lib/overrides');
|
|
5
|
+
const { overrideMessagingFunctions, overrideChatsApi, overrideCoreChatRedirect, overrideCoreChatsPage } = require('./lib/overrides');
|
|
6
6
|
const { overrideForActivityTracking, backfillActivitySet } = require('./lib/activity');
|
|
7
7
|
|
|
8
8
|
const plugin = Object.assign({}, hooks);
|
|
@@ -12,6 +12,7 @@ plugin.init = async function (params) {
|
|
|
12
12
|
overrideMessagingFunctions();
|
|
13
13
|
overrideChatsApi();
|
|
14
14
|
overrideCoreChatRedirect(params.controllers, renderAdminChatsPage);
|
|
15
|
+
overrideCoreChatsPage(params.controllers);
|
|
15
16
|
overrideForActivityTracking();
|
|
16
17
|
await backfillActivitySet();
|
|
17
18
|
};
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Core's chat list search (public/src/client/chats/search.js) always searches
|
|
4
|
+
// the caller's own rooms. On the admin all-chats page and on another user's
|
|
5
|
+
// chats page that lists the viewer's chats instead of the ones on screen, and
|
|
6
|
+
// opening one of them lands on /user/<other>/chats/<roomId> for a room that
|
|
7
|
+
// user is not in. Core binds its listener with no handle to remove it, so the
|
|
8
|
+
// input is swapped for a clone and searched through the plugin's endpoint.
|
|
9
|
+
$(document).ready(function () {
|
|
10
|
+
function getSearchUid() {
|
|
11
|
+
const data = ajaxify && ajaxify.data;
|
|
12
|
+
if (!data) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (data.adminAllChats) {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
const uid = parseInt(data.uid, 10) || 0;
|
|
19
|
+
if (!uid || uid === parseInt(app.user.uid, 10)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return uid;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function search(query, uid) {
|
|
26
|
+
const params = new URLSearchParams({ query });
|
|
27
|
+
if (uid) {
|
|
28
|
+
params.set('uid', uid);
|
|
29
|
+
}
|
|
30
|
+
const response = await fetch(`${config.relative_path || ''}/api/admin-chats/search?${params.toString()}`, {
|
|
31
|
+
credentials: 'same-origin',
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Received ${response.status}`);
|
|
35
|
+
}
|
|
36
|
+
return await response.json();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function takeOverSearch() {
|
|
40
|
+
const uid = getSearchUid();
|
|
41
|
+
const searchEl = document.getElementById('search-chats');
|
|
42
|
+
const listEl = document.getElementById('private-rooms');
|
|
43
|
+
if (uid === null || !searchEl || !listEl || searchEl.hasAttribute('data-admin-chats-search')) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const replacement = searchEl.cloneNode(true);
|
|
48
|
+
replacement.value = searchEl.value;
|
|
49
|
+
replacement.setAttribute('data-admin-chats-search', '1');
|
|
50
|
+
searchEl.replaceWith(replacement);
|
|
51
|
+
|
|
52
|
+
let lastQuery = replacement.value.trim();
|
|
53
|
+
replacement.addEventListener('keyup', utils.debounce(async function () {
|
|
54
|
+
const query = replacement.value.trim();
|
|
55
|
+
if (query === lastQuery) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
lastQuery = query;
|
|
59
|
+
try {
|
|
60
|
+
const data = await search(query, uid);
|
|
61
|
+
if (query !== lastQuery) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const html = await app.parseAndTranslate('chats', 'rooms', data);
|
|
65
|
+
$(listEl).html(html);
|
|
66
|
+
html.find('.timeago').timeago();
|
|
67
|
+
listEl.setAttribute('data-nextstart', data.nextStart || 0);
|
|
68
|
+
listEl.setAttribute('data-prevstart', 0);
|
|
69
|
+
// Both infinite-scroll loaders (core's and switch-chat.js) bail out
|
|
70
|
+
// while `loading` is set; keep them from appending the unfiltered
|
|
71
|
+
// list under the search results.
|
|
72
|
+
listEl.toggleAttribute('loading', !!query);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
app.alertError(err);
|
|
75
|
+
}
|
|
76
|
+
}, 300));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
$(window).on('action:chat.loaded', takeOverSearch);
|
|
80
|
+
});
|
|
@@ -192,6 +192,12 @@ $(document).ready(function () {
|
|
|
192
192
|
if (!isAdminAllChatsPage()) {
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
|
+
// On back/forward away from the chats page ajaxify.data still describes the chats
|
|
196
|
+
// page until the next page finishes loading. Without this check the room list is
|
|
197
|
+
// "switched" to and /chats is pushed on top of the page the user is navigating to.
|
|
198
|
+
if (!/\/chats(\/|$)/.test(window.location.pathname)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
195
201
|
|
|
196
202
|
const urlMatch = window.location.pathname.match(/\/chats\/(\d+)/);
|
|
197
203
|
const urlRoomId = urlMatch ? parseInt(urlMatch[1], 10) : 0;
|