nodebb-plugin-admin-chats 3.4.0 → 3.4.2

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.
Files changed (3) hide show
  1. package/client.js +89 -49
  2. package/library.js +20 -11
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -223,54 +223,92 @@ $(document).ready(function() {
223
223
  }
224
224
 
225
225
  roomId = roomId || '';
226
- Chats.destroyAutoComplete(ajaxify.data.roomId);
227
- socket.emit('modules.chats.leave', ajaxify.data.roomId);
226
+ // NodeBB 4.14.9+ (56c6115a61): destroyAutoComplete takes the input element, not a roomId
227
+ Chats.destroyAutoComplete($('[component="chat/main-wrapper"]').find('[component="chat/input"]'));
228
+ if (ajaxify.data.roomId) {
229
+ socket.emit('modules.chats.leave', ajaxify.data.roomId);
230
+ }
228
231
 
229
232
  const url = getAdminChatsPageUrl(roomId);
233
+
234
+ async function renderAdminChatWindow(payload) {
235
+ const html = await app.parseAndTranslate('partials/chats/message-window', payload);
236
+ const mainWrapper = $('[component="chat/main-wrapper"]');
237
+ mainWrapper.html(html);
238
+ mainWrapper.attr('data-roomid', roomId);
239
+ html.find('.timeago').timeago();
240
+ ajaxify.data = { ...ajaxify.data, ...payload, roomId: roomId };
241
+ ajaxify.updateTitle(ajaxify.data.title);
242
+ $('body').toggleClass('chat-loaded', !!roomId);
243
+ $('[component="chat/nav-wrapper"]').attr('data-loaded', roomId ? '1' : '0');
244
+ mainWrapper.find('[data-bs-toggle="tooltip"]').tooltip({ trigger: 'hover', container: '#content' });
245
+ Chats.setActive(roomId);
246
+ Chats.addEventListeners();
247
+ // Pass the container like core does — listeners (e.g. the emoji
248
+ // plugin's chat button) resolve elements relative to it
249
+ $(window).trigger('action:chat.loaded', $('.chats-full'));
250
+ if (roomId) {
251
+ ChatsMessages.scrollToBottomAfterImageLoad(mainWrapper.find('[component="chat/message/content"]'));
252
+ }
253
+ if (history.pushState) {
254
+ const fullUrl = `${window.location.protocol}//${window.location.host}${config.relative_path || ''}/${url}`;
255
+ // When the address bar already shows the target (e.g. closing a chat
256
+ // after a back-gesture), replace instead of stacking a duplicate entry
257
+ const sameUrl = window.location.href.split('?')[0] === fullUrl.split('?')[0];
258
+ history[sameUrl ? 'replaceState' : 'pushState']({ url: url }, null, fullUrl);
259
+ }
260
+ bindAdminRecentChatsInfiniteScroll();
261
+ }
262
+
263
+ if (!roomId) {
264
+ // Closing a chat needs no server round-trip — render the empty
265
+ // window from the data we already have (instant back-gesture)
266
+ renderAdminChatWindow({ ...ajaxify.data, roomId: 0, title: '[[pages:chats]]' });
267
+ return;
268
+ }
269
+
230
270
  fetch(getAdminChatsDataUrl(roomId), { credentials: 'include' })
231
271
  .then(async function(response) {
232
272
  if (!response.ok) {
233
273
  throw new Error(`Received ${response.status}`);
234
274
  }
235
-
236
- const payload = await response.json();
237
- const html = await app.parseAndTranslate('partials/chats/message-window', payload);
238
- const mainWrapper = $('[component="chat/main-wrapper"]');
239
- mainWrapper.html(html);
240
- mainWrapper.attr('data-roomid', roomId);
241
- html.find('.timeago').timeago();
242
- ajaxify.data = { ...ajaxify.data, ...payload, roomId: roomId };
243
- ajaxify.updateTitle(ajaxify.data.title);
244
- $('body').toggleClass('chat-loaded', !!roomId);
245
- mainWrapper.find('[data-bs-toggle="tooltip"]').tooltip({ trigger: 'hover', container: '#content' });
246
- Chats.setActive(roomId);
247
- Chats.addEventListeners();
248
- $(window).trigger('action:chat.loaded');
249
- if (roomId) {
250
- ChatsMessages.scrollToBottomAfterImageLoad(mainWrapper.find('[component="chat/message/content"]'));
251
- }
252
- if (history.pushState) {
253
- history.pushState({ url: url }, null, `${window.location.protocol}//${window.location.host}${config.relative_path || ''}/${url}`);
254
- }
255
- bindAdminRecentChatsInfiniteScroll();
275
+ await renderAdminChatWindow(await response.json());
276
+ })
277
+ .catch(function() {
278
+ // Never leave the user stuck on the list — fall back to a
279
+ // regular ajaxify navigation to the room
280
+ ajaxify.go(url);
256
281
  });
257
282
  };
258
283
 
259
284
  Chats._adminAllChatsPatched = true;
260
285
  }
261
286
 
262
- // Auto-open chat if roomId is specified in URL
287
+ // Auto-open chat if roomId is specified in URL and the room is not
288
+ // already rendered (server-side render sets data-roomid on the wrapper —
289
+ // re-switching in that case caused a visible list->chat flash on mobile)
263
290
  const currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
264
- if (currentRoomId && !$('body').hasClass('chat-loaded')) {
291
+ const activeChatId = $('[component="chat/main-wrapper"]').attr('data-roomid') || '';
292
+ if (currentRoomId && String(currentRoomId) !== String(activeChatId)) {
265
293
  setTimeout(function() {
266
294
  Chats.switchChat(currentRoomId);
267
295
  }, 100);
296
+ } else {
297
+ syncChatLoadedState();
268
298
  }
269
299
 
270
300
  bindAdminRecentChatsInfiniteScroll();
271
301
  });
272
302
  }
273
303
 
304
+ // Keep the mobile layout state (body.chat-loaded / nav data-loaded) in sync with
305
+ // the room that is actually rendered, without re-fetching it
306
+ function syncChatLoadedState() {
307
+ const activeChatId = parseInt($('[component="chat/main-wrapper"]').attr('data-roomid'), 10) || 0;
308
+ $('body').toggleClass('chat-loaded', !!activeChatId);
309
+ $('[component="chat/nav-wrapper"]').attr('data-loaded', activeChatId ? '1' : '0');
310
+ }
311
+
274
312
  function replaceAdminEmptyStateText() {
275
313
  if (!hasAdminChatsAccess()) {
276
314
  return;
@@ -353,7 +391,8 @@ $(document).ready(function() {
353
391
 
354
392
  // Try admin endpoint first if user has access
355
393
  if (hasAdminChatsAccess()) {
356
- const adminEndpoint = `${config.relative_path || ''}/api/admin-chats/page/${roomId}`;
394
+ // switch=1: room data only — no need to rebuild the full room list here
395
+ const adminEndpoint = `${config.relative_path || ''}/api/admin-chats/page/${roomId}?switch=1`;
357
396
  try {
358
397
  const adminResponse = await fetch(adminEndpoint, {
359
398
  headers: {
@@ -675,9 +714,10 @@ $(document).ready(function() {
675
714
  patchForumChatsForAdminAll();
676
715
  bindAdminRecentChatsInfiniteScroll();
677
716
 
678
- // Auto-open chat if roomId is specified in URL and chat is not already loaded
717
+ // Auto-open chat if roomId is specified in URL and that room is not already rendered
679
718
  const currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
680
- if (currentRoomId && !$('body').hasClass('chat-loaded')) {
719
+ const activeChatId = $('[component="chat/main-wrapper"]').attr('data-roomid') || '';
720
+ if (currentRoomId && String(currentRoomId) !== String(activeChatId)) {
681
721
  setTimeout(function() {
682
722
  require(['forum/chats'], function(Chats) {
683
723
  if (Chats && Chats.switchChat) {
@@ -685,6 +725,8 @@ $(document).ready(function() {
685
725
  }
686
726
  });
687
727
  }, 200);
728
+ } else {
729
+ syncChatLoadedState();
688
730
  }
689
731
  }
690
732
 
@@ -718,6 +760,8 @@ $(document).ready(function() {
718
760
  }
719
761
  });
720
762
  }, 100);
763
+ } else {
764
+ syncChatLoadedState();
721
765
  }
722
766
  }
723
767
  refreshChatUi();
@@ -776,32 +820,28 @@ $(document).ready(function() {
776
820
  }
777
821
  });
778
822
 
779
- // Handle URL changes for internal navigation
823
+ // Handle URL changes for internal navigation (including back/forward gestures).
824
+ // The URL is the source of truth here — ajaxify.data.roomId is stale after popstate.
780
825
  function handleChatNavigation() {
781
826
  if (!isAdminAllChatsPage()) {
782
827
  return;
783
828
  }
784
-
785
- // Try to get roomId from multiple sources
786
- let currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
787
-
788
- // If not found in ajaxify.data, try to extract from URL
789
- if (!currentRoomId) {
790
- const urlMatch = window.location.pathname.match(/\/chats\/(\d+)/);
791
- if (urlMatch) {
792
- currentRoomId = parseInt(urlMatch[1], 10);
793
- }
794
- }
795
-
796
- const activeChatId = $('[component="chat/main-wrapper"]').attr('data-roomid');
797
-
798
- if (currentRoomId && (!activeChatId || activeChatId !== String(currentRoomId))) {
799
- require(['forum/chats'], function(Chats) {
800
- if (Chats && Chats.switchChat) {
801
- Chats.switchChat(currentRoomId);
802
- }
803
- });
829
+
830
+ const urlMatch = window.location.pathname.match(/\/chats\/(\d+)/);
831
+ const urlRoomId = urlMatch ? parseInt(urlMatch[1], 10) : 0;
832
+ const activeChatId = parseInt($('[component="chat/main-wrapper"]').attr('data-roomid'), 10) || 0;
833
+
834
+ if (urlRoomId === activeChatId) {
835
+ return;
804
836
  }
837
+
838
+ require(['forum/chats'], function(Chats) {
839
+ if (Chats && Chats.switchChat) {
840
+ // urlRoomId 0 -> URL is the chat list: close the open chat and show the list
841
+ // (previously a back-gesture out of a chat left the chat stuck on screen)
842
+ Chats.switchChat(urlRoomId || '');
843
+ }
844
+ });
805
845
  }
806
846
 
807
847
  $(document).on('click', '[data-action="delete"], [data-action="restore"]', function() {
package/library.js CHANGED
@@ -847,7 +847,9 @@ function registerRoutes(app, router, middleware) {
847
847
  return;
848
848
  }
849
849
 
850
- const payload = await buildAdminChatsPayload(req);
850
+ const payload = await buildAdminChatsPayload(req, {
851
+ isSwitch: parseInt(req.query.switch, 10) === 1,
852
+ });
851
853
  res.json(payload);
852
854
  } catch (err) {
853
855
  res.status(500).json({ status: { code: 'error', message: err.message } });
@@ -1115,7 +1117,11 @@ async function assertAdminChatsAccess(req, res) {
1115
1117
  return true;
1116
1118
  }
1117
1119
 
1118
- async function buildAdminChatsPayload(req) {
1120
+ async function buildAdminChatsPayload(req, options = {}) {
1121
+ // Like core (controllers/accounts/chats.js): when the client only switches
1122
+ // rooms (?switch=1 on the JSON endpoint), skip rebuilding the room list —
1123
+ // scanning every chat room on each switch made room-opening slow on mobile
1124
+ const isSwitch = !!options.isSwitch;
1119
1125
  const userslug = await User.getUserField(req.uid, 'userslug');
1120
1126
  const [isAdmin, canView, canManage] = await Promise.all([
1121
1127
  User.isAdministrator(req.uid),
@@ -1124,11 +1130,6 @@ async function buildAdminChatsPayload(req) {
1124
1130
  ]);
1125
1131
  const adminChatsManage = isAdmin || canManage;
1126
1132
  const adminChatsAccess = isAdmin || canView || canManage;
1127
- const [recentChats, publicRooms, privateRoomCount] = await Promise.all([
1128
- getAdminRecentChats(req.uid, 0, ADMIN_CHAT_PAGE_SIZE),
1129
- Messaging.getPublicRooms(req.uid, req.uid),
1130
- getPrivateRoomCount(),
1131
- ]);
1132
1133
 
1133
1134
  const payload = {
1134
1135
  title: '[[pages:chats]]',
@@ -1138,13 +1139,21 @@ async function buildAdminChatsPayload(req) {
1138
1139
  adminChatsAccess,
1139
1140
  adminChatsIsAdmin: isAdmin,
1140
1141
  adminChatsCanManage: adminChatsManage,
1141
- rooms: recentChats.rooms,
1142
- nextStart: recentChats.nextStart,
1143
- publicRooms: publicRooms || [],
1144
- privateRoomCount,
1145
1142
  bodyClasses: ['page-user-chats'],
1146
1143
  };
1147
1144
 
1145
+ if (!isSwitch) {
1146
+ const [recentChats, publicRooms, privateRoomCount] = await Promise.all([
1147
+ getAdminRecentChats(req.uid, 0, ADMIN_CHAT_PAGE_SIZE),
1148
+ Messaging.getPublicRooms(req.uid, req.uid),
1149
+ getPrivateRoomCount(),
1150
+ ]);
1151
+ payload.rooms = recentChats.rooms;
1152
+ payload.nextStart = recentChats.nextStart;
1153
+ payload.publicRooms = publicRooms || [];
1154
+ payload.privateRoomCount = privateRoomCount;
1155
+ }
1156
+
1148
1157
  const roomId = parseInt(req.params.roomId, 10) || 0;
1149
1158
  if (!roomId) {
1150
1159
  return payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-admin-chats",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "description": "A plugin that allows the administrator to view and manage user chats.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {