nodebb-plugin-admin-chats 3.4.3 → 3.5.1

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/client.js DELETED
@@ -1,920 +0,0 @@
1
- $(document).ready(function() {
2
-
3
- // Initialize translations object if not already set
4
- if (!window.adminChatsTranslations) {
5
- window.adminChatsTranslations = {};
6
- }
7
-
8
-
9
- // Get translations from ajaxify.data if available
10
- if (ajaxify.data && ajaxify.data.adminChatsTranslations) {
11
- window.adminChatsTranslations = ajaxify.data.adminChatsTranslations;
12
- window.adminChatsLanguage = ajaxify.data.adminChatsLanguage;
13
- window.adminChatsAccess = ajaxify.data.adminChatsAccess;
14
- window.adminChatsIsAdmin = ajaxify.data.adminChatsIsAdmin;
15
- window.adminChatsCanManage = ajaxify.data.adminChatsCanManage;
16
- }
17
-
18
- syncAdminAccessFromAjaxify();
19
- fetchAdminPrivileges();
20
- const LOCK_PREFIX = '[admin-chat-lock]';
21
- const roomStateCache = new Map();
22
-
23
- function t(key) {
24
- // Get translation from server (from language files)
25
- if (window.adminChatsTranslations && window.adminChatsTranslations[key]) {
26
- return window.adminChatsTranslations[key];
27
- }
28
- // If translation not found, log warning and return the key itself
29
- return key;
30
- }
31
-
32
- function isEnglishSystem() {
33
- const lang = window.adminChatsLanguage || $('html').attr('lang') || 'en';
34
- const langCode = lang.split('-')[0];
35
- return langCode === 'en';
36
- }
37
-
38
- function hasAdminChatsAccess() {
39
- return !!((ajaxify && ajaxify.data && (ajaxify.data.adminChatsAccess || ajaxify.data.adminChatsCanManage)) || window.adminChatsAccess || window.adminChatsCanManage || (app.user && app.user.isAdmin));
40
- }
41
-
42
- function isAdminUser() {
43
- return !!((ajaxify && ajaxify.data && ajaxify.data.adminChatsIsAdmin) || window.adminChatsIsAdmin || (app.user && app.user.isAdmin));
44
- }
45
-
46
- async function fetchAdminPrivileges() {
47
- if (!hasAdminChatsAccess() || window.adminChatsPrivilegesPending) {
48
- return;
49
- }
50
-
51
- window.adminChatsPrivilegesPending = true;
52
- try {
53
- const response = await fetch(`${config.relative_path || ''}/api/admin-chats/privileges`, {
54
- headers: {
55
- 'x-csrf-token': config.csrf_token,
56
- },
57
- credentials: 'same-origin',
58
- });
59
-
60
- if (!response.ok) {
61
- return;
62
- }
63
-
64
- const payload = await response.json();
65
- if (payload && typeof payload.adminChatsAccess === 'boolean') {
66
- window.adminChatsAccess = payload.adminChatsAccess;
67
- }
68
- if (payload && typeof payload.adminChatsCanManage === 'boolean') {
69
- window.adminChatsCanManage = payload.adminChatsCanManage;
70
- }
71
- if (payload && typeof payload.adminChatsIsAdmin === 'boolean') {
72
- window.adminChatsIsAdmin = payload.adminChatsIsAdmin;
73
- }
74
- } catch (err) {
75
- // ignore
76
- } finally {
77
- window.adminChatsPrivilegesPending = false;
78
- }
79
- }
80
-
81
- function syncAdminAccessFromAjaxify() {
82
- if (!ajaxify || !ajaxify.data) {
83
- return;
84
- }
85
- if (typeof ajaxify.data.adminChatsAccess === 'boolean') {
86
- window.adminChatsAccess = ajaxify.data.adminChatsAccess;
87
- }
88
- if (typeof ajaxify.data.adminChatsCanManage === 'boolean') {
89
- window.adminChatsCanManage = ajaxify.data.adminChatsCanManage;
90
- }
91
- if (typeof ajaxify.data.adminChatsIsAdmin === 'boolean') {
92
- window.adminChatsIsAdmin = ajaxify.data.adminChatsIsAdmin;
93
- }
94
- }
95
-
96
- function canManageChats() {
97
- return !!((ajaxify && ajaxify.data && ajaxify.data.adminChatsCanManage) || window.adminChatsCanManage || isAdminUser());
98
- }
99
-
100
- let adminChatsAccessProbe = null;
101
-
102
- function resolveAdminChatsAccess() {
103
- if (hasAdminChatsAccess()) {
104
- return Promise.resolve(true);
105
- }
106
- if (window.adminChatsAccessChecked) {
107
- return Promise.resolve(false);
108
- }
109
- if (adminChatsAccessProbe) {
110
- return adminChatsAccessProbe;
111
- }
112
-
113
- adminChatsAccessProbe = fetch(`${config.relative_path || ''}/api/admin-chats?start=0`, {
114
- headers: {
115
- 'x-csrf-token': config.csrf_token,
116
- },
117
- credentials: 'same-origin',
118
- }).then((response) => {
119
- const ok = response && response.ok;
120
- window.adminChatsAccess = ok;
121
- return ok;
122
- }).catch(() => {
123
- window.adminChatsAccess = false;
124
- return false;
125
- }).finally(() => {
126
- window.adminChatsAccessChecked = true;
127
- adminChatsAccessProbe = null;
128
- });
129
-
130
- return adminChatsAccessProbe;
131
- }
132
-
133
- // Get lockedAction message from server translations only (based on forum settings)
134
- function getLockedActionMessage() {
135
- if (window.adminChatsTranslations && window.adminChatsTranslations['errors.lockedAction']) {
136
- return window.adminChatsTranslations['errors.lockedAction'];
137
- }
138
- // If server translation not available, default to English
139
- return 'This action cannot be performed in a locked room.';
140
- }
141
-
142
- function isAdminAllChatsPage() {
143
- return !!(ajaxify && ajaxify.data && ajaxify.data.adminAllChats);
144
- }
145
-
146
- function getAdminChatsPageUrl(roomId) {
147
- return roomId ? `chats/${roomId}` : 'chats';
148
- }
149
-
150
- function getAdminChatsDataUrl(roomId) {
151
- const params = new URL(document.location).searchParams;
152
- params.set('switch', 1);
153
- const query = params.toString();
154
- return `${config.relative_path || ''}/api/admin-chats/page${roomId ? `/${roomId}` : ''}${query ? `?${query}` : ''}`;
155
- }
156
-
157
- async function loadMoreAdminChats() {
158
- const recentChats = $('[component="chat/recent"]');
159
- if (!isAdminAllChatsPage() || !recentChats.length || recentChats.attr('loading')) {
160
- return;
161
- }
162
-
163
- recentChats.attr('loading', 1);
164
-
165
- try {
166
- const start = parseInt(recentChats.attr('data-nextstart'), 10) || 0;
167
- const response = await fetch(`${config.relative_path || ''}/api/admin-chats?start=${start}`, {
168
- headers: {
169
- 'x-csrf-token': config.csrf_token,
170
- },
171
- credentials: 'same-origin',
172
- });
173
-
174
- if (!response.ok) {
175
- throw new Error('Unable to load chats');
176
- }
177
-
178
- const payload = await response.json();
179
- if (payload.rooms && payload.rooms.length) {
180
- payload.loadingMore = true;
181
- const html = await app.parseAndTranslate('chats', 'rooms', payload);
182
- recentChats.append(html);
183
- html.find('.timeago').timeago();
184
- recentChats.attr('data-nextstart', payload.nextStart);
185
- }
186
- } catch (err) {
187
- app.alertError(err);
188
- } finally {
189
- recentChats.removeAttr('loading');
190
- }
191
- }
192
-
193
- function bindAdminRecentChatsInfiniteScroll() {
194
- if (!isAdminAllChatsPage()) {
195
- return;
196
- }
197
-
198
- $('[component="chat/recent"]').off('scroll').on('scroll', utils.debounce(function() {
199
- const $this = $(this);
200
- const bottom = ($this[0].scrollHeight - $this.height()) * 0.9;
201
- if ($this.scrollTop() > bottom) {
202
- loadMoreAdminChats();
203
- }
204
- }, 100));
205
- }
206
-
207
- function patchForumChatsForAdminAll() {
208
- if (!isAdminAllChatsPage()) {
209
- return;
210
- }
211
-
212
- require(['forum/chats', 'forum/chats/messages'], function(Chats, ChatsMessages) {
213
- if (!Chats) {
214
- return;
215
- }
216
-
217
- if (!Chats._adminAllChatsPatched) {
218
- const originalSwitchChat = Chats.switchChat;
219
-
220
- Chats.switchChat = function(roomId) {
221
- if (!isAdminAllChatsPage()) {
222
- return originalSwitchChat.call(this, roomId);
223
- }
224
-
225
- roomId = 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
- }
231
-
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
- // Fire through the hooks module like core does — a raw jQuery
248
- // trigger never reaches hooks.on() listeners (e.g. the reactions
249
- // plugin), only legacy $(window).on() ones (e.g. the emoji plugin).
250
- // hooks.fire invokes both kinds and passes the container.
251
- require(['hooks'], function(hooks) {
252
- hooks.fire('action:chat.loaded', $('.chats-full'));
253
- });
254
- if (roomId) {
255
- ChatsMessages.scrollToBottomAfterImageLoad(mainWrapper.find('[component="chat/message/content"]'));
256
- // Core only marks a room read when its list element carries the
257
- // "unread" class — the admin list doesn't, so mark it explicitly
258
- require(['api'], function(api) {
259
- api.del(`/chats/${roomId}/state`, {}).catch(function() {});
260
- });
261
- $('[component="chat/nav-wrapper"]').find(`[data-roomid="${roomId}"]`).removeClass('unread');
262
- }
263
- if (history.pushState) {
264
- const fullUrl = `${window.location.protocol}//${window.location.host}${config.relative_path || ''}/${url}`;
265
- // When the address bar already shows the target (e.g. closing a chat
266
- // after a back-gesture), replace instead of stacking a duplicate entry
267
- const sameUrl = window.location.href.split('?')[0] === fullUrl.split('?')[0];
268
- history[sameUrl ? 'replaceState' : 'pushState']({ url: url }, null, fullUrl);
269
- }
270
- bindAdminRecentChatsInfiniteScroll();
271
- }
272
-
273
- if (!roomId) {
274
- // Closing a chat needs no server round-trip — render the empty
275
- // window from the data we already have (instant back-gesture)
276
- renderAdminChatWindow({ ...ajaxify.data, roomId: 0, title: '[[pages:chats]]' });
277
- return;
278
- }
279
-
280
- fetch(getAdminChatsDataUrl(roomId), { credentials: 'include' })
281
- .then(async function(response) {
282
- if (!response.ok) {
283
- throw new Error(`Received ${response.status}`);
284
- }
285
- await renderAdminChatWindow(await response.json());
286
- })
287
- .catch(function() {
288
- // Never leave the user stuck on the list — fall back to a
289
- // regular ajaxify navigation to the room
290
- ajaxify.go(url);
291
- });
292
- };
293
-
294
- Chats._adminAllChatsPatched = true;
295
- }
296
-
297
- // Auto-open chat if roomId is specified in URL and the room is not
298
- // already rendered (server-side render sets data-roomid on the wrapper —
299
- // re-switching in that case caused a visible list->chat flash on mobile)
300
- const currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
301
- const activeChatId = $('[component="chat/main-wrapper"]').attr('data-roomid') || '';
302
- if (currentRoomId && String(currentRoomId) !== String(activeChatId)) {
303
- setTimeout(function() {
304
- Chats.switchChat(currentRoomId);
305
- }, 100);
306
- } else {
307
- syncChatLoadedState();
308
- }
309
-
310
- bindAdminRecentChatsInfiniteScroll();
311
- });
312
- }
313
-
314
- // Keep the mobile layout state (body.chat-loaded / nav data-loaded) in sync with
315
- // the room that is actually rendered, without re-fetching it
316
- function syncChatLoadedState() {
317
- const activeChatId = parseInt($('[component="chat/main-wrapper"]').attr('data-roomid'), 10) || 0;
318
- $('body').toggleClass('chat-loaded', !!activeChatId);
319
- $('[component="chat/nav-wrapper"]').attr('data-loaded', activeChatId ? '1' : '0');
320
- }
321
-
322
- function replaceAdminEmptyStateText() {
323
- if (!hasAdminChatsAccess()) {
324
- return;
325
- }
326
-
327
- $('span.text-muted.text-sm').each(function() {
328
- const currentText = $(this).text().trim();
329
- if (
330
- currentText.includes("אין לכם צ'אטים פעילים") ||
331
- currentText === "אין לכם צ'אטים פעילים." ||
332
- currentText.includes('You have no active chats') ||
333
- currentText === 'You have no active chats.'
334
- ) {
335
- $(this).text(t('empty.selectChat'));
336
- $(this).removeClass('text-muted');
337
- }
338
- });
339
- }
340
-
341
- function getChatWindows() {
342
- const windows = new Set($('[component="chat/message/window"]').toArray());
343
- $('.chat-modal').each(function() {
344
- const $modal = $(this);
345
- const nested = $modal.find('[component="chat/message/window"]').toArray();
346
- if (nested.length) {
347
- nested.forEach(el => windows.delete(el));
348
- windows.add(this);
349
- } else {
350
- windows.add(this);
351
- }
352
- });
353
- return $(Array.from(windows));
354
- }
355
-
356
- function getWindowRoomId($window) {
357
- const roomId = $window.find('[component="chat/messages"]').first().attr('data-roomid') ||
358
- $window.closest('.chat-modal').attr('data-roomid') ||
359
- (ajaxify && ajaxify.data && ajaxify.data.roomId);
360
- return parseInt(roomId, 10) || 0;
361
- }
362
-
363
- function getCachedRoomData(roomId) {
364
- const cached = roomStateCache.get(String(roomId));
365
- if (!cached) {
366
- return null;
367
- }
368
- if (Date.now() - cached.timestamp > 10000) {
369
- roomStateCache.delete(String(roomId));
370
- return null;
371
- }
372
- return cached.data;
373
- }
374
-
375
- function setCachedRoomData(roomId, data) {
376
- roomStateCache.set(String(roomId), {
377
- data,
378
- timestamp: Date.now(),
379
- });
380
- }
381
-
382
- async function fetchRoomData(roomId) {
383
- if (!roomId) {
384
- return null;
385
- }
386
-
387
- // First, check if we have the room data in ajaxify.data (from the current page)
388
- const currentRoom = ajaxify && ajaxify.data && (ajaxify.data.room || ajaxify.data);
389
- if (currentRoom && parseInt(currentRoom.roomId, 10) === roomId) {
390
- if (currentRoom.adminChatLock) {
391
- setCachedRoomData(roomId, currentRoom);
392
- return currentRoom;
393
- }
394
- }
395
-
396
- // Check cache
397
- const cached = getCachedRoomData(roomId);
398
- if (cached) {
399
- return cached;
400
- }
401
-
402
- // Try admin endpoint first if user has access
403
- if (hasAdminChatsAccess()) {
404
- // switch=1: room data only — no need to rebuild the full room list here
405
- const adminEndpoint = `${config.relative_path || ''}/api/admin-chats/page/${roomId}?switch=1`;
406
- try {
407
- const adminResponse = await fetch(adminEndpoint, {
408
- headers: {
409
- 'x-csrf-token': config.csrf_token,
410
- },
411
- credentials: 'same-origin',
412
- });
413
- if (adminResponse.ok) {
414
- const adminPayload = await adminResponse.json();
415
- const adminRoom = adminPayload && adminPayload.roomId ? adminPayload : (adminPayload && adminPayload.room ? adminPayload.room : null);
416
- if (adminRoom) {
417
- setCachedRoomData(roomId, adminRoom);
418
- return adminRoom;
419
- }
420
- }
421
- } catch (err) {
422
- // Continue to fallback
423
- }
424
- }
425
-
426
- // For all users, try to get lock data from the new endpoint
427
- const lockEndpoint = `${config.relative_path || ''}/api/admin-chats/${roomId}/lock`;
428
- try {
429
- const lockResponse = await fetch(lockEndpoint, {
430
- headers: {
431
- 'x-csrf-token': config.csrf_token,
432
- },
433
- credentials: 'same-origin',
434
- });
435
- if (lockResponse.ok) {
436
- const lockPayload = await lockResponse.json();
437
- if (lockPayload && lockPayload.lockData) {
438
- const roomData = {
439
- roomId: roomId,
440
- adminChatLock: lockPayload.lockData,
441
- };
442
- setCachedRoomData(roomId, roomData);
443
- return roomData;
444
- }
445
- }
446
- } catch (err) {
447
- // Ignore error
448
- }
449
-
450
- return null;
451
- }
452
- function isLockedForUser(roomData) {
453
- return !!(roomData && roomData.adminChatLock && roomData.adminChatLock.isLocked && !canManageChats());
454
- }
455
-
456
- function setComposerHidden($window, hidden) {
457
- $window.find('[component="chat/composer"]').each(function() {
458
- const $composer = $(this);
459
- if (hidden) {
460
- $composer.addClass('hidden').hide();
461
- } else {
462
- $composer.removeClass('hidden').show();
463
- }
464
- });
465
-
466
- $window.find('[component="chat/input"], [component="chat/send"], button[data-action="send"], textarea.chat-input')
467
- .prop('disabled', hidden)
468
- .attr('disabled', hidden ? 'disabled' : null);
469
- }
470
-
471
- function renderLockBanner($window, hidden) {
472
- $window.find('.admin-chat-lock-banner').remove();
473
- if (!hidden) {
474
- return;
475
- }
476
-
477
- const target = $window.find('[component="chat/messages"]').first();
478
- if (!target.length) {
479
- return;
480
- }
481
-
482
- const positionStyle = isEnglishSystem() ? 'float:right; clear:both;' : 'float:left; clear:both;';
483
- target.prepend(`<div class="admin-chat-lock-banner alert alert-warning mb-2 text-start" style="${positionStyle} max-width: fit-content;">${t('lock.banner')}</div>`);
484
- }
485
-
486
- function updateLockedActionVisibility($window, hidden) {
487
- [
488
- '[data-action="reply"]',
489
- '[data-action="edit"]',
490
- '[data-action="delete"]',
491
- '[data-action="restore"]',
492
- '[data-action="kick"]',
493
- '[data-action="toggleOwner"]'
494
- ].forEach(function(selector) {
495
- $window.find(selector).toggleClass('hidden', hidden).toggle(!hidden);
496
- });
497
-
498
- $window.find('[component="chat/controlsToggle"]').closest('.dropdown').toggleClass('hidden', hidden).toggle(!hidden);
499
- $window.find('[component="chat/manage/user/add/search"], [component="chat/manage/user/list/search"], [component="chat/manage/save"]')
500
- .toggleClass('hidden', hidden)
501
- .toggle(!hidden)
502
- .prop('disabled', hidden)
503
- .attr('disabled', hidden ? 'disabled' : null);
504
- $window.find('[component="chat/manage-modal"] .form-text, [component="chat/manage-modal"] .text-danger')
505
- .toggleClass('hidden', hidden);
506
- }
507
-
508
- function updateDeleteRestoreVisibility($window) {
509
- $window.find('[component="chat/message"]').each(function() {
510
- const $message = $(this);
511
- const isDeleted = $message.hasClass('deleted');
512
- $message.find('[data-action="delete"]').toggleClass('hidden', isDeleted).toggle(!isDeleted);
513
- $message.find('[data-action="restore"]').toggleClass('hidden', !isDeleted).toggle(isDeleted);
514
- });
515
- }
516
-
517
- function normalizeLockMessages($window) {
518
- $window.find('[component="chat/system-message"] > div').each(function() {
519
- const $el = $(this);
520
- const text = $el.text().trim();
521
- if (!text.includes(LOCK_PREFIX.slice(1, -1)) && !text.includes('admin-chat-lock')) {
522
- return;
523
- }
524
- $el.text(t('lock.banner'));
525
- });
526
- }
527
-
528
- function getRoomMenuTargets($window) {
529
- const scoped = $window.find('[component="chat/controls"]');
530
- if (scoped.length) {
531
- return scoped;
532
- }
533
- return $window.find('[component="chat/header"], .chat-header, .modal-header, [component="chat/nav"]').find('.dropdown-menu');
534
- }
535
-
536
- function renderAdminLockControl($window, roomData) {
537
- $window.find('.admin-chat-lock-toggle-item, .admin-chat-lock-divider, .admin-chat-lock-item-wrap').remove();
538
-
539
- if (!canManageChats() || !roomData || !roomData.roomId) {
540
- return;
541
- }
542
-
543
- const menus = getRoomMenuTargets($window);
544
- if (!menus.length) {
545
- return;
546
- }
547
-
548
- const lockData = roomData.adminChatLock || {};
549
- const isLocked = !!lockData.isLocked;
550
- const itemText = isLocked ? t('menu.release') : t('menu.lock');
551
- const iconClass = isLocked ? 'fa-lock-open' : 'fa-lock';
552
- const menuItemHtml = `
553
- <li role="presentation" class="admin-chat-lock-item-wrap">
554
- <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}">
555
- <i class="fa fa-fw ${iconClass}"></i>
556
- <span>${itemText}</span>
557
- </a>
558
- </li>
559
- <li role="presentation" class="dropdown-divider admin-chat-lock-divider"></li>
560
- `;
561
-
562
- menus.each(function() {
563
- const $menu = $(this);
564
- if ($menu.find('.admin-chat-lock-toggle-item').length) {
565
- return;
566
- }
567
- $menu.prepend(menuItemHtml);
568
- });
569
- }
570
-
571
- async function applyUiToWindow($window) {
572
- const roomId = getWindowRoomId($window);
573
- if (!roomId) {
574
- return;
575
- }
576
-
577
- const roomData = await fetchRoomData(roomId);
578
- if (!roomData) {
579
- return;
580
- }
581
-
582
- const hidden = isLockedForUser(roomData);
583
- setComposerHidden($window, hidden);
584
- updateLockedActionVisibility($window, hidden);
585
- updateDeleteRestoreVisibility($window);
586
- renderLockBanner($window, hidden);
587
- renderAdminLockControl($window, roomData);
588
- normalizeLockMessages($window);
589
- }
590
-
591
-
592
- function getAdminRoomsMap() {
593
- const rooms = ajaxify && ajaxify.data && Array.isArray(ajaxify.data.rooms) ? ajaxify.data.rooms : [];
594
- return new Map(rooms.map(room => [String(room.roomId), room]));
595
- }
596
-
597
- function renderAdminParticipants() {
598
- if (!isAdminAllChatsPage()) {
599
- return;
600
- }
601
-
602
- const roomsMap = getAdminRoomsMap();
603
- $('[component="chat/recent/room"]').each(function() {
604
- const $room = $(this);
605
- const roomId = String($room.attr('data-roomid') || '');
606
- const roomData = roomsMap.get(roomId);
607
- const $container = $room.find('[component="chat/room/title"]').closest('.d-flex.flex-grow-1.flex-column.w-100');
608
-
609
- $container.find('.admin-chat-participants').remove();
610
- if (!$container.length || !roomData || !roomData.participantsLabel) {
611
- return;
612
- }
613
-
614
- 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>`;
615
- $container.find('[component="chat/room/title"]').after(html);
616
- });
617
- }
618
-
619
- function applyAdminAllPageTweaks() {
620
- if (!isAdminAllChatsPage()) {
621
- return;
622
- }
623
-
624
- $('[component="chat/recent"] .mark-read').addClass('hidden');
625
- $('[data-action="leave"]').closest('li').addClass('hidden');
626
- renderAdminParticipants();
627
- }
628
- async function refreshChatUi() {
629
- replaceAdminEmptyStateText();
630
- applyAdminAllPageTweaks();
631
- const windows = getChatWindows();
632
- await Promise.all(windows.map(function() {
633
- return applyUiToWindow($(this));
634
- }).get());
635
- }
636
-
637
- async function toggleRoomLock(roomId, nextState) {
638
- const response = await fetch(`${config.relative_path || ''}/api/admin-chats/${roomId}/lock`, {
639
- method: 'POST',
640
- headers: {
641
- 'Content-Type': 'application/json',
642
- 'x-csrf-token': config.csrf_token,
643
- },
644
- body: JSON.stringify({ locked: nextState }),
645
- credentials: 'same-origin',
646
- });
647
-
648
- if (!response.ok) {
649
- throw new Error('Unable to update room lock');
650
- }
651
-
652
- return await response.json();
653
- }
654
-
655
- async function injectProfileChatsMenu() {
656
- const allowed = await resolveAdminChatsAccess();
657
- if (!allowed) {
658
- return;
659
- }
660
-
661
- const isUserProfile = $(".account").length && window.location.pathname.includes("/user/");
662
- if (!isUserProfile) {
663
- return;
664
- }
665
-
666
- const userSlug = (ajaxify && ajaxify.data && (ajaxify.data.userslug || (ajaxify.data.user && ajaxify.data.user.userslug))) || null;
667
- if (!userSlug) {
668
- return;
669
- }
670
-
671
- const relativePath = config.relative_path || "";
672
- const menuItemHtml = `
673
- <li role="presentation" class="admin-chats-profile-link">
674
- <a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="${relativePath}/user/${userSlug}/chats" role="menuitem">
675
- <i class="far fa-fw fa-comments"></i>
676
- <span>${t("profile.viewChats")}</span>
677
- </a>
678
- </li>
679
- `;
680
-
681
- let menu = $(".account-sub-links");
682
- if (!menu.length) {
683
- const container = $(".account .flex-shrink-0.d-flex.gap-1").first();
684
- const fallbackContainer = container.length ? container : $(".account .flex-shrink-0").first();
685
-
686
- if (fallbackContainer.length) {
687
- const menuHtml = `
688
- <div class="btn-group bottom-sheet admin-chats-privileges-menu">
689
- <button type="button" class="btn btn-light dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
690
- <i class="fa fa-gear fa-fw"></i>
691
- </button>
692
- <ul class="dropdown-menu dropdown-menu-end p-1 text-sm account-sub-links" role="menu"></ul>
693
- </div>
694
- `;
695
- fallbackContainer.append(menuHtml);
696
- menu = fallbackContainer.find(".account-sub-links").last();
697
- }
698
- }
699
-
700
- if (menu.length) {
701
- menu.find(".admin-chats-profile-link").remove();
702
- menu.find(".admin-chats-profile-divider").remove();
703
- menu.find(`a[href*="/user/${userSlug}/chats"]`).parent().remove();
704
- menu.prepend(menuItemHtml);
705
-
706
- const hasOtherItems = menu.find("li").not(".admin-chats-profile-link").length > 0;
707
- if (hasOtherItems) {
708
- menu.find(".admin-chats-profile-link").after('<li role="presentation" class="dropdown-divider admin-chats-profile-divider"></li>');
709
- }
710
- }
711
- }
712
-
713
- $(window).on('action:ajaxify.end', function(ev, data) {
714
- syncAdminAccessFromAjaxify();
715
- fetchAdminPrivileges();
716
- const templateName = ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '';
717
-
718
- if (templateName.startsWith('account/')) {
719
- injectProfileChatsMenu();
720
- setTimeout(injectProfileChatsMenu, 200);
721
- }
722
-
723
- if (isAdminAllChatsPage()) {
724
- patchForumChatsForAdminAll();
725
- bindAdminRecentChatsInfiniteScroll();
726
-
727
- // Auto-open chat if roomId is specified in URL and that room is not already rendered
728
- const currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
729
- const activeChatId = $('[component="chat/main-wrapper"]').attr('data-roomid') || '';
730
- if (currentRoomId && String(currentRoomId) !== String(activeChatId)) {
731
- setTimeout(function() {
732
- require(['forum/chats'], function(Chats) {
733
- if (Chats && Chats.switchChat) {
734
- Chats.switchChat(currentRoomId);
735
- }
736
- });
737
- }, 200);
738
- } else {
739
- syncChatLoadedState();
740
- }
741
- }
742
-
743
- const url = data && data.url ? data.url : '';
744
- if (url.match(/^user\/.+\/chats/) || url.match(/^chats(\/|$)/)) {
745
- refreshChatUi();
746
- setTimeout(refreshChatUi, 500);
747
- setTimeout(refreshChatUi, 1200);
748
-
749
- // Handle chat navigation for internal URL changes
750
- if (url.match(/^chats\/\d+/)) {
751
- setTimeout(handleChatNavigation, 300);
752
- }
753
- }
754
- });
755
-
756
- $(window).on('action:chat.loaded', function() {
757
- if (isAdminAllChatsPage()) {
758
- patchForumChatsForAdminAll();
759
- bindAdminRecentChatsInfiniteScroll();
760
-
761
- // Auto-open chat if roomId is specified in URL and no chat is currently active
762
- const currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
763
- const activeChatId = $('[component="chat/main-wrapper"]').attr('data-roomid');
764
-
765
- if (currentRoomId && (!activeChatId || activeChatId !== String(currentRoomId))) {
766
- setTimeout(function() {
767
- require(['forum/chats'], function(Chats) {
768
- if (Chats && Chats.switchChat) {
769
- Chats.switchChat(currentRoomId);
770
- }
771
- });
772
- }, 100);
773
- } else {
774
- syncChatLoadedState();
775
- }
776
- }
777
- refreshChatUi();
778
- setTimeout(refreshChatUi, 200);
779
- setTimeout(refreshChatUi, 1000);
780
- });
781
-
782
- $(window).on('action:chat.closed', function() {
783
- setTimeout(refreshChatUi, 200);
784
- });
785
-
786
- $(window).on('action:chat.onMessagesAddedToDom action:chat.edited action:chat.renamed', function() {
787
- setTimeout(refreshChatUi, 0);
788
- });
789
-
790
- if (window.socket) {
791
- socket.removeListener('event:chats.delete', refreshChatUi);
792
- socket.removeListener('event:chats.restore', refreshChatUi);
793
- socket.on('event:chats.delete', function() { setTimeout(refreshChatUi, 0); });
794
- socket.on('event:chats.restore', function() { setTimeout(refreshChatUi, 0); });
795
- }
796
-
797
- // Monitor for new chat modals/windows being added to the DOM
798
- // This ensures styling is applied to floating chat windows opened from non-chat pages
799
- const observer = new MutationObserver(function(mutations) {
800
- let hasNewChatWindow = false;
801
- mutations.forEach(function(mutation) {
802
- if (mutation.type === 'childList') {
803
- mutation.addedNodes.forEach(function(node) {
804
- if (node.nodeType === 1) { // Element node
805
- const $node = $(node);
806
- if ($node.is('.chat-modal') || $node.find('.chat-modal').length) {
807
- hasNewChatWindow = true;
808
- }
809
- if ($node.is('[component="chat/message/window"]') || $node.find('[component="chat/message/window"]').length) {
810
- hasNewChatWindow = true;
811
- }
812
- }
813
- });
814
- }
815
- });
816
- if (hasNewChatWindow) {
817
- setTimeout(refreshChatUi, 50);
818
- }
819
- });
820
-
821
- observer.observe(document.body, {
822
- childList: true,
823
- subtree: true
824
- });
825
-
826
- // Handle browser back/forward navigation
827
- $(window).on('popstate', function() {
828
- if (isAdminAllChatsPage()) {
829
- setTimeout(handleChatNavigation, 100);
830
- }
831
- });
832
-
833
- // Handle URL changes for internal navigation (including back/forward gestures).
834
- // The URL is the source of truth here — ajaxify.data.roomId is stale after popstate.
835
- function handleChatNavigation() {
836
- if (!isAdminAllChatsPage()) {
837
- return;
838
- }
839
-
840
- const urlMatch = window.location.pathname.match(/\/chats\/(\d+)/);
841
- const urlRoomId = urlMatch ? parseInt(urlMatch[1], 10) : 0;
842
- const activeChatId = parseInt($('[component="chat/main-wrapper"]').attr('data-roomid'), 10) || 0;
843
-
844
- if (urlRoomId === activeChatId) {
845
- return;
846
- }
847
-
848
- require(['forum/chats'], function(Chats) {
849
- if (Chats && Chats.switchChat) {
850
- // urlRoomId 0 -> URL is the chat list: close the open chat and show the list
851
- // (previously a back-gesture out of a chat left the chat stuck on screen)
852
- Chats.switchChat(urlRoomId || '');
853
- }
854
- });
855
- }
856
-
857
- $(document).on('click', '[data-action="delete"], [data-action="restore"]', function() {
858
- const $window = $(this).closest('[component="chat/message/window"]');
859
- if (!$window.length) {
860
- return;
861
- }
862
- setTimeout(function() {
863
- updateDeleteRestoreVisibility($window);
864
- }, 50);
865
- });
866
-
867
- $(document).on('click', '.admin-chat-lock-toggle-item', async function(ev) {
868
- ev.preventDefault();
869
-
870
- const $button = $(this);
871
- const roomId = parseInt($button.attr('data-room-id'), 10);
872
- const isLocked = $button.attr('data-locked') === 'true';
873
-
874
- if (!roomId) {
875
- return;
876
- }
877
-
878
- $button.addClass('disabled').attr('aria-disabled', 'true');
879
-
880
- try {
881
- const result = await toggleRoomLock(roomId, !isLocked);
882
- const currentRoom = ajaxify && ajaxify.data && (ajaxify.data.room || ajaxify.data);
883
- if (currentRoom && parseInt(currentRoom.roomId, 10) === roomId) {
884
- currentRoom.adminChatLock = result.lockData;
885
- currentRoom.canReply = !result.lockData.isLocked || canManageChats();
886
- currentRoom.showUserInput = currentRoom.canReply;
887
- if (ajaxify.data.room) {
888
- ajaxify.data.room.adminChatLock = result.lockData;
889
- ajaxify.data.room.canReply = currentRoom.canReply;
890
- ajaxify.data.room.showUserInput = currentRoom.showUserInput;
891
- }
892
- }
893
-
894
- const cached = getCachedRoomData(roomId) || { roomId };
895
- cached.adminChatLock = result.lockData;
896
- cached.canReply = !result.lockData.isLocked || canManageChats();
897
- cached.showUserInput = cached.canReply;
898
- setCachedRoomData(roomId, cached);
899
- await refreshChatUi();
900
- } catch (err) {
901
- app.alertError(t('errors.update'));
902
- $button.removeClass('disabled').removeAttr('aria-disabled');
903
- }
904
- });
905
-
906
- // Intercept and translate error messages from API - only for lockedAction
907
- if (app.alertError && !app.alertError._adminChatsWrapped) {
908
- const originalAlertError = app.alertError;
909
- app.alertError = function(message) {
910
- if (typeof message === 'string') {
911
- if (message.includes('[[admin-chats:errors.lockedAction]]') || message.includes('errors.lockedAction')) {
912
- message = getLockedActionMessage();
913
- }
914
- }
915
- return originalAlertError.call(this, message);
916
- };
917
- app.alertError._adminChatsWrapped = true;
918
- }
919
- });
920
-