nodebb-plugin-admin-chats 1.1.0 → 2.1.0

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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "kiroAgent.configureMCP": "Disabled"
3
+ }
package/client.js CHANGED
@@ -1,150 +1,349 @@
1
1
  $(document).ready(function() {
2
2
  const LOCK_PREFIX = '[admin-chat-lock]';
3
- const LOCKED_MESSAGE_TEXT = '🔒 חדר זה ננעל ע"י המנהלים.';
3
+ const roomStateCache = new Map();
4
+ const TEXT = {
5
+ he: {
6
+ lockBanner: '🔒 חדר זה ננעל ע"י המנהלים.',
7
+ menuLock: 'נעל חדר',
8
+ menuRelease: 'שחרר נעילה',
9
+ updateError: 'לא ניתן לעדכן את נעילת החדר.',
10
+ viewChats: "צפיה בצ'אטים",
11
+ emptyState: "אנא בחר צ'אט מסרגל הצד.",
12
+ participants: 'משתתפים',
13
+ },
14
+ en: {
15
+ lockBanner: '🔒 This room was locked by the administrators.',
16
+ menuLock: 'Lock Room',
17
+ menuRelease: 'Release Lock',
18
+ updateError: 'Unable to update room lock.',
19
+ viewChats: 'View Chats',
20
+ emptyState: 'Please select a chat from the sidebar.',
21
+ participants: 'Participants',
22
+ },
23
+ };
4
24
 
5
25
  function isEnglishSystem() {
6
26
  return $('html').attr('lang') && $('html').attr('lang').startsWith('en');
7
27
  }
8
28
 
9
- function replaceAdminEmptyStateText() {
10
- if (!app.user.isAdmin) return;
29
+ function t(key) {
30
+ const dict = isEnglishSystem() ? TEXT.en : TEXT.he;
31
+ return dict[key];
32
+ }
11
33
 
12
- $('span.text-muted.text-sm').each(function() {
13
- const currentText = $(this).text().trim();
34
+ // Get lockedAction message from server translations only (based on forum settings)
35
+ function getLockedActionMessage() {
36
+ if (window.adminChatsTranslations && window.adminChatsTranslations.lockedAction) {
37
+ return window.adminChatsTranslations.lockedAction;
38
+ }
39
+ // If server translation not available, default to English
40
+ return 'This action cannot be performed in a locked room.';
41
+ }
14
42
 
15
- if (currentText.includes("אין לכם צ'אטים פעילים") || currentText === "אין לכם צ'אטים פעילים.") {
16
- $(this).text("אנא בחר צ'אט מסרגל הצד.");
17
- $(this).removeClass('text-muted');
43
+ function isAdminAllChatsPage() {
44
+ return !!(ajaxify && ajaxify.data && ajaxify.data.adminAllChats);
45
+ }
46
+
47
+ function getAdminChatsPageUrl(roomId) {
48
+ return roomId ? `chats/${roomId}` : 'chats';
49
+ }
50
+
51
+ function getAdminChatsDataUrl(roomId) {
52
+ const params = new URL(document.location).searchParams;
53
+ params.set('switch', 1);
54
+ const query = params.toString();
55
+ return `${config.relative_path || ''}/api/admin-chats/page${roomId ? `/${roomId}` : ''}${query ? `?${query}` : ''}`;
56
+ }
57
+
58
+ async function loadMoreAdminChats() {
59
+ const recentChats = $('[component="chat/recent"]');
60
+ if (!isAdminAllChatsPage() || !recentChats.length || recentChats.attr('loading')) {
61
+ return;
62
+ }
63
+
64
+ recentChats.attr('loading', 1);
65
+
66
+ try {
67
+ const start = parseInt(recentChats.attr('data-nextstart'), 10) || 0;
68
+ const response = await fetch(`${config.relative_path || ''}/api/admin-chats?start=${start}`, {
69
+ headers: {
70
+ 'x-csrf-token': config.csrf_token,
71
+ },
72
+ credentials: 'same-origin',
73
+ });
74
+
75
+ if (!response.ok) {
76
+ throw new Error('Unable to load chats');
18
77
  }
19
78
 
20
- if (currentText.includes('You have no active chats') || currentText === 'You have no active chats.') {
21
- $(this).text('Please select a chat from the sidebar.');
22
- $(this).removeClass('text-muted');
79
+ const payload = await response.json();
80
+ if (payload.rooms && payload.rooms.length) {
81
+ payload.loadingMore = true;
82
+ const html = await app.parseAndTranslate('chats', 'rooms', payload);
83
+ recentChats.append(html);
84
+ html.find('.timeago').timeago();
85
+ recentChats.attr('data-nextstart', payload.nextStart);
23
86
  }
87
+ } catch (err) {
88
+ app.alertError(err);
89
+ } finally {
90
+ recentChats.removeAttr('loading');
91
+ }
92
+ }
93
+
94
+ function bindAdminRecentChatsInfiniteScroll() {
95
+ if (!isAdminAllChatsPage()) {
96
+ return;
97
+ }
98
+
99
+ $('[component="chat/recent"]').off('scroll').on('scroll', utils.debounce(function() {
100
+ const $this = $(this);
101
+ const bottom = ($this[0].scrollHeight - $this.height()) * 0.9;
102
+ if ($this.scrollTop() > bottom) {
103
+ loadMoreAdminChats();
104
+ }
105
+ }, 100));
106
+ }
107
+
108
+ function patchForumChatsForAdminAll() {
109
+ if (!isAdminAllChatsPage()) {
110
+ return;
111
+ }
112
+
113
+ require(['forum/chats', 'forum/chats/messages'], function(Chats, ChatsMessages) {
114
+ if (!Chats) {
115
+ return;
116
+ }
117
+
118
+ if (!Chats._adminAllChatsPatched) {
119
+ const originalSwitchChat = Chats.switchChat;
120
+
121
+ Chats.switchChat = function(roomId) {
122
+ if (!isAdminAllChatsPage()) {
123
+ return originalSwitchChat.call(this, roomId);
124
+ }
125
+
126
+ roomId = roomId || '';
127
+ Chats.destroyAutoComplete(ajaxify.data.roomId);
128
+ socket.emit('modules.chats.leave', ajaxify.data.roomId);
129
+
130
+ const url = getAdminChatsPageUrl(roomId);
131
+ fetch(getAdminChatsDataUrl(roomId), { credentials: 'include' })
132
+ .then(async function(response) {
133
+ if (!response.ok) {
134
+ throw new Error(`Received ${response.status}`);
135
+ }
136
+
137
+ const payload = await response.json();
138
+ const html = await app.parseAndTranslate('partials/chats/message-window', payload);
139
+ const mainWrapper = $('[component="chat/main-wrapper"]');
140
+ mainWrapper.html(html);
141
+ mainWrapper.attr('data-roomid', roomId);
142
+ html.find('.timeago').timeago();
143
+ ajaxify.data = { ...ajaxify.data, ...payload, roomId: roomId };
144
+ ajaxify.updateTitle(ajaxify.data.title);
145
+ $('body').toggleClass('chat-loaded', !!roomId);
146
+ mainWrapper.find('[data-bs-toggle="tooltip"]').tooltip({ trigger: 'hover', container: '#content' });
147
+ Chats.setActive(roomId);
148
+ Chats.addEventListeners();
149
+ $(window).trigger('action:chat.loaded');
150
+ if (roomId) {
151
+ ChatsMessages.scrollToBottomAfterImageLoad(mainWrapper.find('[component="chat/message/content"]'));
152
+ }
153
+ if (history.pushState) {
154
+ history.pushState({ url: url }, null, `${window.location.protocol}//${window.location.host}${config.relative_path || ''}/${url}`);
155
+ }
156
+ bindAdminRecentChatsInfiniteScroll();
157
+ });
158
+ };
159
+
160
+ Chats._adminAllChatsPatched = true;
161
+ }
162
+
163
+ bindAdminRecentChatsInfiniteScroll();
24
164
  });
25
165
  }
26
166
 
27
- function getRoomData() {
28
- if (!ajaxify || !ajaxify.data) {
29
- return null;
167
+ function replaceAdminEmptyStateText() {
168
+ if (!app.user.isAdmin) {
169
+ return;
30
170
  }
31
171
 
32
- return ajaxify.data.room || ajaxify.data;
172
+ $('span.text-muted.text-sm').each(function() {
173
+ const currentText = $(this).text().trim();
174
+ if (
175
+ currentText.includes("אין לכם צ'אטים פעילים") ||
176
+ currentText === "אין לכם צ'אטים פעילים." ||
177
+ currentText.includes('You have no active chats') ||
178
+ currentText === 'You have no active chats.'
179
+ ) {
180
+ $(this).text(t('emptyState'));
181
+ $(this).removeClass('text-muted');
182
+ }
183
+ });
184
+ }
185
+
186
+ function getChatWindows() {
187
+ return $('[component="chat/message/window"]');
188
+ }
189
+
190
+ function getWindowRoomId($window) {
191
+ const roomId = $window.find('[component="chat/messages"]').first().attr('data-roomid') ||
192
+ $window.closest('.chat-modal').attr('data-roomid') ||
193
+ (ajaxify && ajaxify.data && ajaxify.data.roomId);
194
+ return parseInt(roomId, 10) || 0;
33
195
  }
34
196
 
35
- function getLockText() {
36
- if (isEnglishSystem()) {
37
- return '🔒 This room was locked by the administrators.';
197
+ function getCachedRoomData(roomId) {
198
+ const cached = roomStateCache.get(String(roomId));
199
+ if (!cached) {
200
+ return null;
201
+ }
202
+ if (Date.now() - cached.timestamp > 10000) {
203
+ roomStateCache.delete(String(roomId));
204
+ return null;
38
205
  }
206
+ return cached.data;
207
+ }
39
208
 
40
- return LOCKED_MESSAGE_TEXT;
209
+ function setCachedRoomData(roomId, data) {
210
+ roomStateCache.set(String(roomId), {
211
+ data,
212
+ timestamp: Date.now(),
213
+ });
41
214
  }
42
215
 
43
- function renderLockBanner() {
44
- const roomData = getRoomData();
45
- const lockData = roomData && roomData.adminChatLock;
46
- $('.admin-chat-lock-banner').remove();
216
+ async function fetchRoomData(roomId) {
217
+ if (!roomId) {
218
+ return null;
219
+ }
47
220
 
48
- if (!lockData || !lockData.isLocked) {
49
- return;
221
+ const currentRoom = ajaxify && ajaxify.data && (ajaxify.data.room || ajaxify.data);
222
+ if (currentRoom && parseInt(currentRoom.roomId, 10) === roomId && currentRoom.adminChatLock) {
223
+ setCachedRoomData(roomId, currentRoom);
224
+ return currentRoom;
50
225
  }
51
226
 
52
- const target = $('[component="chat/messages"]').first().parent();
53
- if (!target.length) {
54
- return;
227
+ const cached = getCachedRoomData(roomId);
228
+ if (cached) {
229
+ return cached;
230
+ }
231
+
232
+ const endpoint = isAdminAllChatsPage() ?
233
+ `${config.relative_path || ''}/api/admin-chats/page/${roomId}` :
234
+ `${config.relative_path || ''}/api/chats/${roomId}`;
235
+ const response = await fetch(endpoint, {
236
+ headers: {
237
+ 'x-csrf-token': config.csrf_token,
238
+ },
239
+ credentials: 'same-origin',
240
+ });
241
+ if (!response.ok) {
242
+ return null;
55
243
  }
56
244
 
57
- target.prepend(`
58
- <div class="admin-chat-lock-banner alert alert-warning mb-2">${getLockText()}</div>
59
- `);
245
+ const payload = await response.json();
246
+ const roomData = payload && payload.roomId ? payload : (payload && payload.room ? payload.room : null);
247
+ if (roomData) {
248
+ setCachedRoomData(roomId, roomData);
249
+ }
250
+ return roomData;
60
251
  }
61
252
 
62
- function setComposerVisibility(shouldHide) {
63
- const selectors = [
64
- '[component="chat/composer"]',
65
- '[component="chat/replies"]',
66
- '[component="chat/input"]',
67
- '[component="chat/textarea"]',
68
- '.chat-input',
69
- '.composer',
70
- ];
71
-
72
- const seen = new Set();
73
- selectors.forEach(function(selector) {
74
- $(selector).each(function() {
75
- const $element = $(this);
76
- const $container = $element.closest('[component="chat/composer"], [component="chat/replies"], .chat-input, .composer').length ?
77
- $element.closest('[component="chat/composer"], [component="chat/replies"], .chat-input, .composer').first() :
78
- $element;
79
- const domNode = $container.get(0);
80
-
81
- if (!domNode || seen.has(domNode)) {
82
- return;
83
- }
253
+ function isLockedForUser(roomData) {
254
+ return !!(roomData && roomData.adminChatLock && roomData.adminChatLock.isLocked && !(app.user && app.user.isAdmin));
255
+ }
84
256
 
85
- seen.add(domNode);
86
- $container.toggle(!shouldHide);
87
- });
257
+ function setComposerHidden($window, hidden) {
258
+ $window.find('[component="chat/composer"]').each(function() {
259
+ const $composer = $(this);
260
+ if (hidden) {
261
+ $composer.addClass('hidden').hide();
262
+ } else {
263
+ $composer.removeClass('hidden').show();
264
+ }
88
265
  });
266
+
267
+ $window.find('[component="chat/input"], [component="chat/send"], button[data-action="send"], textarea.chat-input')
268
+ .prop('disabled', hidden)
269
+ .attr('disabled', hidden ? 'disabled' : null);
89
270
  }
90
271
 
91
- function updateComposerState() {
92
- const roomData = getRoomData();
93
- const lockData = roomData && roomData.adminChatLock;
94
- const shouldHideComposer = !!(lockData && lockData.isLocked && !(app.user && app.user.isAdmin));
272
+ function renderLockBanner($window, hidden) {
273
+ $window.find('.admin-chat-lock-banner').remove();
274
+ if (!hidden) {
275
+ return;
276
+ }
277
+
278
+ const target = $window.find('[component="chat/messages"]').first();
279
+ if (!target.length) {
280
+ return;
281
+ }
95
282
 
96
- $('[component="chat/input"], [component="chat/textarea"], textarea.chat-input, .chat-input textarea')
97
- .prop('disabled', shouldHideComposer)
98
- .attr('placeholder', shouldHideComposer ? getLockText() : null);
283
+ const positionStyle = isEnglishSystem() ? 'float:right; clear:both;' : 'float:left; clear:both;';
284
+ target.prepend(`<div class="admin-chat-lock-banner alert alert-warning mb-2 text-start" style="${positionStyle} max-width: fit-content;">${t('lockBanner')}</div>`);
285
+ }
99
286
 
100
- $('[component="chat/send"], .chat-send, button[component="chat/submit"]').prop('disabled', shouldHideComposer);
287
+ function updateLockedActionVisibility($window, hidden) {
288
+ [
289
+ '[data-action="reply"]',
290
+ '[data-action="edit"]',
291
+ '[data-action="delete"]',
292
+ '[data-action="restore"]',
293
+ '[data-action="kick"]',
294
+ '[data-action="toggleOwner"]'
295
+ ].forEach(function(selector) {
296
+ $window.find(selector).toggleClass('hidden', hidden).toggle(!hidden);
297
+ });
101
298
 
102
- setComposerVisibility(shouldHideComposer);
103
- renderLockBanner();
299
+ $window.find('[component="chat/controlsToggle"]').closest('.dropdown').toggleClass('hidden', hidden).toggle(!hidden);
300
+ $window.find('[component="chat/manage/user/add/search"], [component="chat/manage/user/list/search"], [component="chat/manage/save"]')
301
+ .toggleClass('hidden', hidden)
302
+ .toggle(!hidden)
303
+ .prop('disabled', hidden)
304
+ .attr('disabled', hidden ? 'disabled' : null);
305
+ $window.find('[component="chat/manage-modal"] .form-text, [component="chat/manage-modal"] .text-danger')
306
+ .toggleClass('hidden', hidden);
104
307
  }
105
308
 
106
- function normalizeLockMessages() {
107
- $('[component="chat/message/content"], .chat-message-content').each(function() {
309
+ function normalizeLockMessages($window) {
310
+ $window.find('[component="chat/system-message"] > div').each(function() {
108
311
  const $el = $(this);
109
312
  const text = $el.text().trim();
110
- if (!text.startsWith(LOCK_PREFIX)) {
313
+ if (!text.includes(LOCK_PREFIX.slice(1, -1)) && !text.includes('admin-chat-lock')) {
111
314
  return;
112
315
  }
113
-
114
- $el.text(text.replace(LOCK_PREFIX, '').trim());
316
+ $el.text(t('lockBanner'));
115
317
  });
116
318
  }
117
319
 
118
- function getRoomMenuTargets() {
119
- return $('[component="chat/header"], .chat-header, [component="chat/nav"]')
120
- .find('.dropdown-menu');
320
+ function getRoomMenuTargets($window) {
321
+ const scoped = $window.find('[component="chat/controls"]');
322
+ if (scoped.length) {
323
+ return scoped;
324
+ }
325
+ return $window.find('[component="chat/header"], .chat-header, .modal-header, [component="chat/nav"]').find('.dropdown-menu');
121
326
  }
122
327
 
123
- function renderAdminLockControl() {
124
- $('.admin-chat-lock-toggle-item, .admin-chat-lock-divider').remove();
328
+ function renderAdminLockControl($window, roomData) {
329
+ $window.find('.admin-chat-lock-toggle-item, .admin-chat-lock-divider, .admin-chat-lock-item-wrap').remove();
125
330
 
126
- if (!app.user.isAdmin) {
127
- return;
128
- }
129
-
130
- const roomData = getRoomData();
131
- const roomId = roomData && roomData.roomId;
132
- if (!roomId) {
331
+ if (!app.user.isAdmin || !roomData || !roomData.roomId) {
133
332
  return;
134
333
  }
135
334
 
136
- const menus = getRoomMenuTargets();
335
+ const menus = getRoomMenuTargets($window);
137
336
  if (!menus.length) {
138
337
  return;
139
338
  }
140
339
 
141
340
  const lockData = roomData.adminChatLock || {};
142
341
  const isLocked = !!lockData.isLocked;
143
- const itemText = isEnglishSystem() ? (isLocked ? 'Unlock Room' : 'Lock Room') : (isLocked ? 'פתח חדר' : 'נעל חדר');
342
+ const itemText = isLocked ? t('menuRelease') : t('menuLock');
144
343
  const iconClass = isLocked ? 'fa-lock-open' : 'fa-lock';
145
344
  const menuItemHtml = `
146
345
  <li role="presentation" class="admin-chat-lock-item-wrap">
147
- <a href="#" role="menuitem" class="dropdown-item rounded-1 d-flex align-items-center gap-2 admin-chat-lock-toggle-item" data-room-id="${roomId}" data-locked="${isLocked}">
346
+ <a href="#" role="menuitem" class="dropdown-item rounded-1 d-flex align-items-center gap-2 admin-chat-lock-toggle-item" data-room-id="${roomData.roomId}" data-locked="${isLocked}">
148
347
  <i class="fa fa-fw ${iconClass}"></i>
149
348
  <span>${itemText}</span>
150
349
  </a>
@@ -157,11 +356,75 @@ $(document).ready(function() {
157
356
  if ($menu.find('.admin-chat-lock-toggle-item').length) {
158
357
  return;
159
358
  }
160
-
161
359
  $menu.prepend(menuItemHtml);
162
360
  });
163
361
  }
164
362
 
363
+ async function applyUiToWindow($window) {
364
+ const roomId = getWindowRoomId($window);
365
+ if (!roomId) {
366
+ return;
367
+ }
368
+
369
+ const roomData = await fetchRoomData(roomId);
370
+ if (!roomData) {
371
+ return;
372
+ }
373
+
374
+ const hidden = isLockedForUser(roomData);
375
+ setComposerHidden($window, hidden);
376
+ updateLockedActionVisibility($window, hidden);
377
+ renderLockBanner($window, hidden);
378
+ renderAdminLockControl($window, roomData);
379
+ normalizeLockMessages($window);
380
+ }
381
+
382
+
383
+ function getAdminRoomsMap() {
384
+ const rooms = ajaxify && ajaxify.data && Array.isArray(ajaxify.data.rooms) ? ajaxify.data.rooms : [];
385
+ return new Map(rooms.map(room => [String(room.roomId), room]));
386
+ }
387
+
388
+ function renderAdminParticipants() {
389
+ if (!isAdminAllChatsPage()) {
390
+ return;
391
+ }
392
+
393
+ const roomsMap = getAdminRoomsMap();
394
+ $('[component="chat/recent/room"]').each(function() {
395
+ const $room = $(this);
396
+ const roomId = String($room.attr('data-roomid') || '');
397
+ const roomData = roomsMap.get(roomId);
398
+ const $container = $room.find('[component="chat/room/title"]').closest('.d-flex.flex-grow-1.flex-column.w-100');
399
+
400
+ $container.find('.admin-chat-participants').remove();
401
+ if (!$container.length || !roomData || !roomData.participantsLabel) {
402
+ return;
403
+ }
404
+
405
+ const html = `<div class="admin-chat-participants text-muted text-xs text-break line-clamp-2"><span class="fw-semibold">${t('participants')}:</span> ${utils.escapeHTML(roomData.participantsLabel)}</div>`;
406
+ $container.find('[component="chat/room/title"]').after(html);
407
+ });
408
+ }
409
+
410
+ function applyAdminAllPageTweaks() {
411
+ if (!isAdminAllChatsPage()) {
412
+ return;
413
+ }
414
+
415
+ $('[component="chat/recent"] .mark-read').addClass('hidden');
416
+ $('[data-action="leave"]').closest('li').addClass('hidden');
417
+ renderAdminParticipants();
418
+ }
419
+ async function refreshChatUi() {
420
+ replaceAdminEmptyStateText();
421
+ applyAdminAllPageTweaks();
422
+ const windows = getChatWindows();
423
+ await Promise.all(windows.map(function() {
424
+ return applyUiToWindow($(this));
425
+ }).get());
426
+ }
427
+
165
428
  async function toggleRoomLock(roomId, nextState) {
166
429
  const response = await fetch(`${config.relative_path || ''}/api/admin-chats/${roomId}/lock`, {
167
430
  method: 'POST',
@@ -170,6 +433,7 @@ $(document).ready(function() {
170
433
  'x-csrf-token': config.csrf_token,
171
434
  },
172
435
  body: JSON.stringify({ locked: nextState }),
436
+ credentials: 'same-origin',
173
437
  });
174
438
 
175
439
  if (!response.ok) {
@@ -179,13 +443,6 @@ $(document).ready(function() {
179
443
  return await response.json();
180
444
  }
181
445
 
182
- function refreshChatUi() {
183
- replaceAdminEmptyStateText();
184
- renderAdminLockControl();
185
- updateComposerState();
186
- normalizeLockMessages();
187
- }
188
-
189
446
  $(window).on('action:ajaxify.end', function(ev, data) {
190
447
  const templateName = ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '';
191
448
 
@@ -193,14 +450,12 @@ $(document).ready(function() {
193
450
  const userSlug = ajaxify.data.userslug || (ajaxify.data.user && ajaxify.data.user.userslug);
194
451
 
195
452
  if (userSlug) {
196
- const buttonText = isEnglishSystem() ? 'View Chats' : "צפיה בצ'אטים";
197
453
  const relativePath = config.relative_path || '';
198
-
199
454
  const btnHtml = `
200
455
  <li role="presentation">
201
456
  <a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="${relativePath}/user/${userSlug}/chats" role="menuitem">
202
457
  <i class="far fa-fw fa-comments"></i>
203
- <span>${buttonText}</span>
458
+ <span>${t('viewChats')}</span>
204
459
  </a>
205
460
  </li>
206
461
  <li role="presentation" class="dropdown-divider"></li>
@@ -213,23 +468,37 @@ $(document).ready(function() {
213
468
  }
214
469
  }
215
470
 
216
- const url = data && data.url ? data.url : '';
471
+ if (isAdminAllChatsPage()) {
472
+ patchForumChatsForAdminAll();
473
+ bindAdminRecentChatsInfiniteScroll();
474
+ }
217
475
 
218
- if (url.match(/^user\/.+\/chats/) || url === 'chats') {
476
+ const url = data && data.url ? data.url : '';
477
+ if (url.match(/^user\/.+\/chats/) || url.match(/^chats(\/|$)/)) {
219
478
  refreshChatUi();
220
479
  setTimeout(refreshChatUi, 500);
480
+ setTimeout(refreshChatUi, 1200);
221
481
  }
222
482
  });
223
483
 
224
484
  $(window).on('action:chat.loaded', function() {
485
+ if (isAdminAllChatsPage()) {
486
+ patchForumChatsForAdminAll();
487
+ bindAdminRecentChatsInfiniteScroll();
488
+ }
225
489
  refreshChatUi();
226
490
  setTimeout(refreshChatUi, 200);
491
+ setTimeout(refreshChatUi, 1000);
227
492
  });
228
493
 
229
494
  $(window).on('action:chat.closed', function() {
230
495
  setTimeout(refreshChatUi, 200);
231
496
  });
232
497
 
498
+ $(window).on('action:chat.onMessagesAddedToDom action:chat.edited action:chat.renamed', function() {
499
+ setTimeout(refreshChatUi, 0);
500
+ });
501
+
233
502
  $(document).on('click', '.admin-chat-lock-toggle-item', async function(ev) {
234
503
  ev.preventDefault();
235
504
 
@@ -245,16 +514,42 @@ $(document).ready(function() {
245
514
 
246
515
  try {
247
516
  const result = await toggleRoomLock(roomId, !isLocked);
248
- if (ajaxify && ajaxify.data) {
249
- ajaxify.data.adminChatLock = result.lockData;
517
+ const currentRoom = ajaxify && ajaxify.data && (ajaxify.data.room || ajaxify.data);
518
+ if (currentRoom && parseInt(currentRoom.roomId, 10) === roomId) {
519
+ currentRoom.adminChatLock = result.lockData;
520
+ currentRoom.canReply = !result.lockData.isLocked || (app.user && app.user.isAdmin);
521
+ currentRoom.showUserInput = currentRoom.canReply;
250
522
  if (ajaxify.data.room) {
251
523
  ajaxify.data.room.adminChatLock = result.lockData;
524
+ ajaxify.data.room.canReply = currentRoom.canReply;
525
+ ajaxify.data.room.showUserInput = currentRoom.showUserInput;
252
526
  }
253
527
  }
254
- ajaxify.refresh();
528
+
529
+ const cached = getCachedRoomData(roomId) || { roomId };
530
+ cached.adminChatLock = result.lockData;
531
+ cached.canReply = !result.lockData.isLocked || (app.user && app.user.isAdmin);
532
+ cached.showUserInput = cached.canReply;
533
+ setCachedRoomData(roomId, cached);
534
+ await refreshChatUi();
255
535
  } catch (err) {
256
- app.alertError(isEnglishSystem() ? 'Unable to update room lock.' : 'לא ניתן לעדכן את נעילת החדר.');
536
+ app.alertError(t('updateError'));
257
537
  $button.removeClass('disabled').removeAttr('aria-disabled');
258
538
  }
259
539
  });
540
+
541
+ // Intercept and translate error messages from API - only for lockedAction
542
+ if (app.alertError && !app.alertError._adminChatsWrapped) {
543
+ const originalAlertError = app.alertError;
544
+ app.alertError = function(message) {
545
+ if (typeof message === 'string') {
546
+ if (message.includes('[[admin-chats:errors.lockedAction]]') || message.includes('errors.lockedAction')) {
547
+ message = getLockedActionMessage();
548
+ }
549
+ }
550
+ return originalAlertError.call(this, message);
551
+ };
552
+ app.alertError._adminChatsWrapped = true;
553
+ }
260
554
  });
555
+