nodebb-plugin-admin-chats 2.0.0 → 2.1.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/.vscode/settings.json +2 -2
- package/README.md +61 -56
- package/client.js +629 -387
- package/library.js +904 -450
- package/package.json +12 -12
- package/plugin.json +79 -64
- package/public/languages/en-GB/admin-chats.json +9 -9
- package/public/languages/he/admin-chats.json +9 -9
package/client.js
CHANGED
|
@@ -1,387 +1,629 @@
|
|
|
1
|
-
$(document).ready(function() {
|
|
2
|
-
const LOCK_PREFIX = '[admin-chat-lock]';
|
|
3
|
-
const roomStateCache = new Map();
|
|
4
|
-
const TEXT = {
|
|
5
|
-
he: {
|
|
6
|
-
lockBanner: '🔒 חדר זה ננעל ע"י המנהלים.',
|
|
7
|
-
menuLock: 'נעל חדר',
|
|
8
|
-
menuRelease: 'שחרר נעילה',
|
|
9
|
-
updateError: 'לא ניתן לעדכן את נעילת החדר.',
|
|
10
|
-
viewChats: "צפיה בצ'אטים",
|
|
11
|
-
emptyState: "אנא בחר צ'אט מסרגל הצד.",
|
|
12
|
-
|
|
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
|
-
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function isEnglishSystem() {
|
|
26
|
-
return $('html').attr('lang') && $('html').attr('lang').startsWith('en');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function t(key) {
|
|
30
|
-
const dict = isEnglishSystem() ? TEXT.en : TEXT.he;
|
|
31
|
-
return dict[key];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
1
|
+
$(document).ready(function() {
|
|
2
|
+
const LOCK_PREFIX = '[admin-chat-lock]';
|
|
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
|
+
};
|
|
24
|
+
|
|
25
|
+
function isEnglishSystem() {
|
|
26
|
+
return $('html').attr('lang') && $('html').attr('lang').startsWith('en');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function t(key) {
|
|
30
|
+
const dict = isEnglishSystem() ? TEXT.en : TEXT.he;
|
|
31
|
+
return dict[key];
|
|
32
|
+
}
|
|
33
|
+
|
|
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
|
+
}
|
|
42
|
+
|
|
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');
|
|
77
|
+
}
|
|
78
|
+
|
|
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);
|
|
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
|
+
// Auto-open chat if roomId is specified in URL
|
|
164
|
+
const currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
|
|
165
|
+
if (currentRoomId && !$('body').hasClass('chat-loaded')) {
|
|
166
|
+
setTimeout(function() {
|
|
167
|
+
Chats.switchChat(currentRoomId);
|
|
168
|
+
}, 100);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
bindAdminRecentChatsInfiniteScroll();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function replaceAdminEmptyStateText() {
|
|
176
|
+
if (!app.user.isAdmin) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
$('span.text-muted.text-sm').each(function() {
|
|
181
|
+
const currentText = $(this).text().trim();
|
|
182
|
+
if (
|
|
183
|
+
currentText.includes("אין לכם צ'אטים פעילים") ||
|
|
184
|
+
currentText === "אין לכם צ'אטים פעילים." ||
|
|
185
|
+
currentText.includes('You have no active chats') ||
|
|
186
|
+
currentText === 'You have no active chats.'
|
|
187
|
+
) {
|
|
188
|
+
$(this).text(t('emptyState'));
|
|
189
|
+
$(this).removeClass('text-muted');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getChatWindows() {
|
|
195
|
+
return $('[component="chat/message/window"]');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getWindowRoomId($window) {
|
|
199
|
+
const roomId = $window.find('[component="chat/messages"]').first().attr('data-roomid') ||
|
|
200
|
+
$window.closest('.chat-modal').attr('data-roomid') ||
|
|
201
|
+
(ajaxify && ajaxify.data && ajaxify.data.roomId);
|
|
202
|
+
return parseInt(roomId, 10) || 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getCachedRoomData(roomId) {
|
|
206
|
+
const cached = roomStateCache.get(String(roomId));
|
|
207
|
+
if (!cached) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
if (Date.now() - cached.timestamp > 10000) {
|
|
211
|
+
roomStateCache.delete(String(roomId));
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return cached.data;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function setCachedRoomData(roomId, data) {
|
|
218
|
+
roomStateCache.set(String(roomId), {
|
|
219
|
+
data,
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function fetchRoomData(roomId) {
|
|
225
|
+
if (!roomId) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const currentRoom = ajaxify && ajaxify.data && (ajaxify.data.room || ajaxify.data);
|
|
230
|
+
if (currentRoom && parseInt(currentRoom.roomId, 10) === roomId && currentRoom.adminChatLock) {
|
|
231
|
+
setCachedRoomData(roomId, currentRoom);
|
|
232
|
+
return currentRoom;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const cached = getCachedRoomData(roomId);
|
|
236
|
+
if (cached) {
|
|
237
|
+
return cached;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const endpoint = isAdminAllChatsPage() ?
|
|
241
|
+
`${config.relative_path || ''}/api/admin-chats/page/${roomId}` :
|
|
242
|
+
`${config.relative_path || ''}/api/chats/${roomId}`;
|
|
243
|
+
const response = await fetch(endpoint, {
|
|
244
|
+
headers: {
|
|
245
|
+
'x-csrf-token': config.csrf_token,
|
|
246
|
+
},
|
|
247
|
+
credentials: 'same-origin',
|
|
248
|
+
});
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const payload = await response.json();
|
|
254
|
+
const roomData = payload && payload.roomId ? payload : (payload && payload.room ? payload.room : null);
|
|
255
|
+
if (roomData) {
|
|
256
|
+
setCachedRoomData(roomId, roomData);
|
|
257
|
+
}
|
|
258
|
+
return roomData;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isLockedForUser(roomData) {
|
|
262
|
+
return !!(roomData && roomData.adminChatLock && roomData.adminChatLock.isLocked && !(app.user && app.user.isAdmin));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function setComposerHidden($window, hidden) {
|
|
266
|
+
$window.find('[component="chat/composer"]').each(function() {
|
|
267
|
+
const $composer = $(this);
|
|
268
|
+
if (hidden) {
|
|
269
|
+
$composer.addClass('hidden').hide();
|
|
270
|
+
} else {
|
|
271
|
+
$composer.removeClass('hidden').show();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
$window.find('[component="chat/input"], [component="chat/send"], button[data-action="send"], textarea.chat-input')
|
|
276
|
+
.prop('disabled', hidden)
|
|
277
|
+
.attr('disabled', hidden ? 'disabled' : null);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function renderLockBanner($window, hidden) {
|
|
281
|
+
$window.find('.admin-chat-lock-banner').remove();
|
|
282
|
+
if (!hidden) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const target = $window.find('[component="chat/messages"]').first();
|
|
287
|
+
if (!target.length) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const positionStyle = isEnglishSystem() ? 'float:right; clear:both;' : 'float:left; clear:both;';
|
|
292
|
+
target.prepend(`<div class="admin-chat-lock-banner alert alert-warning mb-2 text-start" style="${positionStyle} max-width: fit-content;">${t('lockBanner')}</div>`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function updateLockedActionVisibility($window, hidden) {
|
|
296
|
+
[
|
|
297
|
+
'[data-action="reply"]',
|
|
298
|
+
'[data-action="edit"]',
|
|
299
|
+
'[data-action="delete"]',
|
|
300
|
+
'[data-action="restore"]',
|
|
301
|
+
'[data-action="kick"]',
|
|
302
|
+
'[data-action="toggleOwner"]'
|
|
303
|
+
].forEach(function(selector) {
|
|
304
|
+
$window.find(selector).toggleClass('hidden', hidden).toggle(!hidden);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
$window.find('[component="chat/controlsToggle"]').closest('.dropdown').toggleClass('hidden', hidden).toggle(!hidden);
|
|
308
|
+
$window.find('[component="chat/manage/user/add/search"], [component="chat/manage/user/list/search"], [component="chat/manage/save"]')
|
|
309
|
+
.toggleClass('hidden', hidden)
|
|
310
|
+
.toggle(!hidden)
|
|
311
|
+
.prop('disabled', hidden)
|
|
312
|
+
.attr('disabled', hidden ? 'disabled' : null);
|
|
313
|
+
$window.find('[component="chat/manage-modal"] .form-text, [component="chat/manage-modal"] .text-danger')
|
|
314
|
+
.toggleClass('hidden', hidden);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeLockMessages($window) {
|
|
318
|
+
$window.find('[component="chat/system-message"] > div').each(function() {
|
|
319
|
+
const $el = $(this);
|
|
320
|
+
const text = $el.text().trim();
|
|
321
|
+
if (!text.includes(LOCK_PREFIX.slice(1, -1)) && !text.includes('admin-chat-lock')) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
$el.text(t('lockBanner'));
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getRoomMenuTargets($window) {
|
|
329
|
+
const scoped = $window.find('[component="chat/controls"]');
|
|
330
|
+
if (scoped.length) {
|
|
331
|
+
return scoped;
|
|
332
|
+
}
|
|
333
|
+
return $window.find('[component="chat/header"], .chat-header, .modal-header, [component="chat/nav"]').find('.dropdown-menu');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function renderAdminLockControl($window, roomData) {
|
|
337
|
+
$window.find('.admin-chat-lock-toggle-item, .admin-chat-lock-divider, .admin-chat-lock-item-wrap').remove();
|
|
338
|
+
|
|
339
|
+
if (!app.user.isAdmin || !roomData || !roomData.roomId) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const menus = getRoomMenuTargets($window);
|
|
344
|
+
if (!menus.length) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const lockData = roomData.adminChatLock || {};
|
|
349
|
+
const isLocked = !!lockData.isLocked;
|
|
350
|
+
const itemText = isLocked ? t('menuRelease') : t('menuLock');
|
|
351
|
+
const iconClass = isLocked ? 'fa-lock-open' : 'fa-lock';
|
|
352
|
+
const menuItemHtml = `
|
|
353
|
+
<li role="presentation" class="admin-chat-lock-item-wrap">
|
|
354
|
+
<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}">
|
|
355
|
+
<i class="fa fa-fw ${iconClass}"></i>
|
|
356
|
+
<span>${itemText}</span>
|
|
357
|
+
</a>
|
|
358
|
+
</li>
|
|
359
|
+
<li role="presentation" class="dropdown-divider admin-chat-lock-divider"></li>
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
menus.each(function() {
|
|
363
|
+
const $menu = $(this);
|
|
364
|
+
if ($menu.find('.admin-chat-lock-toggle-item').length) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
$menu.prepend(menuItemHtml);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function applyUiToWindow($window) {
|
|
372
|
+
const roomId = getWindowRoomId($window);
|
|
373
|
+
if (!roomId) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const roomData = await fetchRoomData(roomId);
|
|
378
|
+
if (!roomData) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const hidden = isLockedForUser(roomData);
|
|
383
|
+
setComposerHidden($window, hidden);
|
|
384
|
+
updateLockedActionVisibility($window, hidden);
|
|
385
|
+
renderLockBanner($window, hidden);
|
|
386
|
+
renderAdminLockControl($window, roomData);
|
|
387
|
+
normalizeLockMessages($window);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
function getAdminRoomsMap() {
|
|
392
|
+
const rooms = ajaxify && ajaxify.data && Array.isArray(ajaxify.data.rooms) ? ajaxify.data.rooms : [];
|
|
393
|
+
return new Map(rooms.map(room => [String(room.roomId), room]));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function renderAdminParticipants() {
|
|
397
|
+
if (!isAdminAllChatsPage()) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const roomsMap = getAdminRoomsMap();
|
|
402
|
+
$('[component="chat/recent/room"]').each(function() {
|
|
403
|
+
const $room = $(this);
|
|
404
|
+
const roomId = String($room.attr('data-roomid') || '');
|
|
405
|
+
const roomData = roomsMap.get(roomId);
|
|
406
|
+
const $container = $room.find('[component="chat/room/title"]').closest('.d-flex.flex-grow-1.flex-column.w-100');
|
|
407
|
+
|
|
408
|
+
$container.find('.admin-chat-participants').remove();
|
|
409
|
+
if (!$container.length || !roomData || !roomData.participantsLabel) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
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>`;
|
|
414
|
+
$container.find('[component="chat/room/title"]').after(html);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function applyAdminAllPageTweaks() {
|
|
419
|
+
if (!isAdminAllChatsPage()) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
$('[component="chat/recent"] .mark-read').addClass('hidden');
|
|
424
|
+
$('[data-action="leave"]').closest('li').addClass('hidden');
|
|
425
|
+
renderAdminParticipants();
|
|
426
|
+
}
|
|
427
|
+
async function refreshChatUi() {
|
|
428
|
+
replaceAdminEmptyStateText();
|
|
429
|
+
applyAdminAllPageTweaks();
|
|
430
|
+
const windows = getChatWindows();
|
|
431
|
+
await Promise.all(windows.map(function() {
|
|
432
|
+
return applyUiToWindow($(this));
|
|
433
|
+
}).get());
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function toggleRoomLock(roomId, nextState) {
|
|
437
|
+
const response = await fetch(`${config.relative_path || ''}/api/admin-chats/${roomId}/lock`, {
|
|
438
|
+
method: 'POST',
|
|
439
|
+
headers: {
|
|
440
|
+
'Content-Type': 'application/json',
|
|
441
|
+
'x-csrf-token': config.csrf_token,
|
|
442
|
+
},
|
|
443
|
+
body: JSON.stringify({ locked: nextState }),
|
|
444
|
+
credentials: 'same-origin',
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (!response.ok) {
|
|
448
|
+
throw new Error('Unable to update room lock');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return await response.json();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
$(window).on('action:ajaxify.end', function(ev, data) {
|
|
455
|
+
const templateName = ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '';
|
|
456
|
+
|
|
457
|
+
if (app.user.isAdmin && templateName.startsWith('account/')) {
|
|
458
|
+
const userSlug = ajaxify.data.userslug || (ajaxify.data.user && ajaxify.data.user.userslug);
|
|
459
|
+
|
|
460
|
+
if (userSlug) {
|
|
461
|
+
const relativePath = config.relative_path || '';
|
|
462
|
+
const btnHtml = `
|
|
463
|
+
<li role="presentation">
|
|
464
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="${relativePath}/user/${userSlug}/chats" role="menuitem">
|
|
465
|
+
<i class="far fa-fw fa-comments"></i>
|
|
466
|
+
<span>${t('viewChats')}</span>
|
|
467
|
+
</a>
|
|
468
|
+
</li>
|
|
469
|
+
<li role="presentation" class="dropdown-divider"></li>
|
|
470
|
+
`;
|
|
471
|
+
const menu = $('.account-sub-links');
|
|
472
|
+
if (menu.length) {
|
|
473
|
+
menu.find(`a[href*="/user/${userSlug}/chats"]`).parent().remove();
|
|
474
|
+
menu.prepend(btnHtml);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (isAdminAllChatsPage()) {
|
|
480
|
+
patchForumChatsForAdminAll();
|
|
481
|
+
bindAdminRecentChatsInfiniteScroll();
|
|
482
|
+
|
|
483
|
+
// Auto-open chat if roomId is specified in URL and chat is not already loaded
|
|
484
|
+
const currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
|
|
485
|
+
if (currentRoomId && !$('body').hasClass('chat-loaded')) {
|
|
486
|
+
setTimeout(function() {
|
|
487
|
+
require(['forum/chats'], function(Chats) {
|
|
488
|
+
if (Chats && Chats.switchChat) {
|
|
489
|
+
Chats.switchChat(currentRoomId);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}, 200);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const url = data && data.url ? data.url : '';
|
|
497
|
+
if (url.match(/^user\/.+\/chats/) || url.match(/^chats(\/|$)/)) {
|
|
498
|
+
refreshChatUi();
|
|
499
|
+
setTimeout(refreshChatUi, 500);
|
|
500
|
+
setTimeout(refreshChatUi, 1200);
|
|
501
|
+
|
|
502
|
+
// Handle chat navigation for internal URL changes
|
|
503
|
+
if (url.match(/^chats\/\d+/)) {
|
|
504
|
+
setTimeout(handleChatNavigation, 300);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
$(window).on('action:chat.loaded', function() {
|
|
510
|
+
if (isAdminAllChatsPage()) {
|
|
511
|
+
patchForumChatsForAdminAll();
|
|
512
|
+
bindAdminRecentChatsInfiniteScroll();
|
|
513
|
+
|
|
514
|
+
// Auto-open chat if roomId is specified in URL and no chat is currently active
|
|
515
|
+
const currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
|
|
516
|
+
const activeChatId = $('[component="chat/main-wrapper"]').attr('data-roomid');
|
|
517
|
+
|
|
518
|
+
if (currentRoomId && (!activeChatId || activeChatId !== String(currentRoomId))) {
|
|
519
|
+
setTimeout(function() {
|
|
520
|
+
require(['forum/chats'], function(Chats) {
|
|
521
|
+
if (Chats && Chats.switchChat) {
|
|
522
|
+
Chats.switchChat(currentRoomId);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}, 100);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
refreshChatUi();
|
|
529
|
+
setTimeout(refreshChatUi, 200);
|
|
530
|
+
setTimeout(refreshChatUi, 1000);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
$(window).on('action:chat.closed', function() {
|
|
534
|
+
setTimeout(refreshChatUi, 200);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
$(window).on('action:chat.onMessagesAddedToDom action:chat.edited action:chat.renamed', function() {
|
|
538
|
+
setTimeout(refreshChatUi, 0);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Handle browser back/forward navigation
|
|
542
|
+
$(window).on('popstate', function() {
|
|
543
|
+
if (isAdminAllChatsPage()) {
|
|
544
|
+
setTimeout(handleChatNavigation, 100);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Handle URL changes for internal navigation
|
|
549
|
+
function handleChatNavigation() {
|
|
550
|
+
if (!isAdminAllChatsPage()) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Try to get roomId from multiple sources
|
|
555
|
+
let currentRoomId = ajaxify && ajaxify.data && ajaxify.data.roomId;
|
|
556
|
+
|
|
557
|
+
// If not found in ajaxify.data, try to extract from URL
|
|
558
|
+
if (!currentRoomId) {
|
|
559
|
+
const urlMatch = window.location.pathname.match(/\/chats\/(\d+)/);
|
|
560
|
+
if (urlMatch) {
|
|
561
|
+
currentRoomId = parseInt(urlMatch[1], 10);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const activeChatId = $('[component="chat/main-wrapper"]').attr('data-roomid');
|
|
566
|
+
|
|
567
|
+
if (currentRoomId && (!activeChatId || activeChatId !== String(currentRoomId))) {
|
|
568
|
+
require(['forum/chats'], function(Chats) {
|
|
569
|
+
if (Chats && Chats.switchChat) {
|
|
570
|
+
Chats.switchChat(currentRoomId);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
$(document).on('click', '.admin-chat-lock-toggle-item', async function(ev) {
|
|
577
|
+
ev.preventDefault();
|
|
578
|
+
|
|
579
|
+
const $button = $(this);
|
|
580
|
+
const roomId = parseInt($button.attr('data-room-id'), 10);
|
|
581
|
+
const isLocked = $button.attr('data-locked') === 'true';
|
|
582
|
+
|
|
583
|
+
if (!roomId) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
$button.addClass('disabled').attr('aria-disabled', 'true');
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
const result = await toggleRoomLock(roomId, !isLocked);
|
|
591
|
+
const currentRoom = ajaxify && ajaxify.data && (ajaxify.data.room || ajaxify.data);
|
|
592
|
+
if (currentRoom && parseInt(currentRoom.roomId, 10) === roomId) {
|
|
593
|
+
currentRoom.adminChatLock = result.lockData;
|
|
594
|
+
currentRoom.canReply = !result.lockData.isLocked || (app.user && app.user.isAdmin);
|
|
595
|
+
currentRoom.showUserInput = currentRoom.canReply;
|
|
596
|
+
if (ajaxify.data.room) {
|
|
597
|
+
ajaxify.data.room.adminChatLock = result.lockData;
|
|
598
|
+
ajaxify.data.room.canReply = currentRoom.canReply;
|
|
599
|
+
ajaxify.data.room.showUserInput = currentRoom.showUserInput;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const cached = getCachedRoomData(roomId) || { roomId };
|
|
604
|
+
cached.adminChatLock = result.lockData;
|
|
605
|
+
cached.canReply = !result.lockData.isLocked || (app.user && app.user.isAdmin);
|
|
606
|
+
cached.showUserInput = cached.canReply;
|
|
607
|
+
setCachedRoomData(roomId, cached);
|
|
608
|
+
await refreshChatUi();
|
|
609
|
+
} catch (err) {
|
|
610
|
+
app.alertError(t('updateError'));
|
|
611
|
+
$button.removeClass('disabled').removeAttr('aria-disabled');
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// Intercept and translate error messages from API - only for lockedAction
|
|
616
|
+
if (app.alertError && !app.alertError._adminChatsWrapped) {
|
|
617
|
+
const originalAlertError = app.alertError;
|
|
618
|
+
app.alertError = function(message) {
|
|
619
|
+
if (typeof message === 'string') {
|
|
620
|
+
if (message.includes('[[admin-chats:errors.lockedAction]]') || message.includes('errors.lockedAction')) {
|
|
621
|
+
message = getLockedActionMessage();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return originalAlertError.call(this, message);
|
|
625
|
+
};
|
|
626
|
+
app.alertError._adminChatsWrapped = true;
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|