nodebb-plugin-admin-chats 3.5.1 → 3.6.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.
package/lib/history.js CHANGED
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const validator = nodebb.require('validator');
4
3
  const Messaging = nodebb.require('./src/messaging');
5
4
  const User = nodebb.require('./src/user');
6
5
  const db = nodebb.require('./src/database');
@@ -59,6 +58,10 @@ async function getMessageEditHistory(mid) {
59
58
  };
60
59
  };
61
60
 
61
+ // The author of the current version is whoever performed the most recent
62
+ // edit (the newest revision's editor); with no revisions it's the sender.
63
+ const lastRevision = revisions[revisions.length - 1];
64
+
62
65
  return {
63
66
  mid,
64
67
  exists: !!exists,
@@ -69,6 +72,7 @@ async function getMessageEditHistory(mid) {
69
72
  edited: parseInt(current && current.edited, 10) || 0,
70
73
  timestamp: parseInt(current && current.timestamp, 10) || 0,
71
74
  deleted: parseInt(current && current.deleted, 10) === 1,
75
+ editor: decorate(lastRevision ? lastRevision.editedByUid : authorUid),
72
76
  },
73
77
  revisionCount: revisions.length,
74
78
  revisions: revisions.map(rev => ({
@@ -80,65 +84,27 @@ async function getMessageEditHistory(mid) {
80
84
  };
81
85
  }
82
86
 
83
- function renderMessageHistoryHtml(history) {
84
- const esc = value => validator.escape(String(value == null ? '' : value));
85
- const fmt = (ts) => {
86
- const numeric = parseInt(ts, 10) || 0;
87
- return numeric ? new Date(numeric).toISOString().replace('T', ' ').replace(/\..+$/, ' UTC') : '—';
88
- };
87
+ async function deleteRevision(mid, index) {
88
+ const key = editHistoryKey(mid);
89
+ const items = await db.getListRange(key, 0, -1);
90
+ const numericIndex = parseInt(index, 10);
89
91
 
90
- if (!history.exists) {
91
- return `<!doctype html><html lang="he" dir="rtl"><meta charset="utf-8"><title>היסטוריית עריכות</title><body style="font-family:system-ui,Arial,sans-serif;padding:2rem;">
92
- <h1>הודעה ${esc(history.mid)} לא נמצאה</h1></body></html>`;
92
+ if (!Array.isArray(items) || Number.isNaN(numericIndex) || numericIndex < 0 || numericIndex >= items.length) {
93
+ return false;
93
94
  }
94
95
 
95
- const timeline = history.revisions.map((rev, index) => `
96
- <li class="rev">
97
- <div class="meta">גרסה ${index + 1} · הוחלפה ב-${esc(fmt(rev.replacedAt))} · עורך: ${esc(rev.editor.username)}</div>
98
- <pre class="content">${esc(rev.content)}</pre>
99
- </li>`).join('');
100
-
101
- const currentBlock = `
102
- <li class="rev current">
103
- <div class="meta">גרסה נוכחית${history.current.edited ? ` · נערכה לאחרונה ${esc(fmt(history.current.edited))}` : ''}${history.current.deleted ? ' · מחוקה' : ''}</div>
104
- <pre class="content">${esc(history.current.content)}</pre>
105
- </li>`;
106
-
107
- return `<!doctype html>
108
- <html lang="he" dir="rtl">
109
- <head>
110
- <meta charset="utf-8">
111
- <meta name="robots" content="noindex,nofollow">
112
- <title>היסטוריית עריכות · הודעה ${esc(history.mid)}</title>
113
- <style>
114
- body { font-family: system-ui, Segoe UI, Arial, sans-serif; margin: 0; padding: 2rem; background: #f5f6f8; color: #1c1e21; }
115
- h1 { font-size: 1.25rem; margin: 0 0 .25rem; }
116
- .sub { color: #65676b; font-size: .875rem; margin-bottom: 1.5rem; }
117
- ul { list-style: none; margin: 0; padding: 0; max-width: 780px; }
118
- .rev { background: #fff; border: 1px solid #dcdfe3; border-radius: 8px; padding: .75rem 1rem; margin-bottom: 1rem; }
119
- .rev.current { border-color: #2d6cdf; box-shadow: 0 0 0 1px #2d6cdf33; }
120
- .meta { font-size: .8rem; color: #65676b; margin-bottom: .5rem; }
121
- .content { white-space: pre-wrap; word-break: break-word; margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9rem; }
122
- @media (prefers-color-scheme: dark) {
123
- body { background: #18191a; color: #e4e6eb; }
124
- .rev { background: #242526; border-color: #3a3b3c; }
125
- .meta, .sub { color: #b0b3b8; }
96
+ items.splice(numericIndex, 1);
97
+ await db.delete(key);
98
+ for (const item of items) {
99
+ // eslint-disable-next-line no-await-in-loop
100
+ await db.listAppend(key, item);
126
101
  }
127
- </style>
128
- </head>
129
- <body>
130
- <h1>היסטוריית עריכות · הודעה ${esc(history.mid)}</h1>
131
- <div class="sub">מחבר: ${esc(history.author.username)} · חדר: ${esc(history.roomId)} · ${esc(history.revisionCount)} עריכות קודמות</div>
132
- <ul>
133
- ${history.revisionCount ? timeline : '<li class="rev"><div class="meta">אין עריכות קודמות מתועדות</div></li>'}
134
- ${currentBlock}
135
- </ul>
136
- </body>
137
- </html>`;
102
+
103
+ return true;
138
104
  }
139
105
 
140
106
  module.exports = {
141
107
  editHistoryKey,
142
108
  getMessageEditHistory,
143
- renderMessageHistoryHtml,
109
+ deleteRevision,
144
110
  };
package/lib/routes.js CHANGED
@@ -6,11 +6,10 @@ const db = nodebb.require('./src/database');
6
6
  const meta = nodebb.require('./src/meta');
7
7
  const privileges = nodebb.require('./src/privileges');
8
8
  const helpers = nodebb.require('./src/controllers/helpers');
9
- const validator = nodebb.require('validator');
10
9
  const sockets = nodebb.require('./src/socket.io');
11
10
 
12
11
  const { canAccessAdminChats, canManageAdminChats } = require('./access');
13
- const { getMessageEditHistory, renderMessageHistoryHtml } = require('./history');
12
+ const { getMessageEditHistory, deleteRevision } = require('./history');
14
13
  const { roomExists, lockRoom, unlockRoom, getRoomLockData } = require('./lock');
15
14
 
16
15
  const ADMIN_CHAT_PAGE_SIZE = 30;
@@ -374,19 +373,17 @@ function registerRoutes(app, router, middleware) {
374
373
  }
375
374
  });
376
375
 
377
- // --- Hidden chat edit-history viewer ---------------------------------
378
- // These two routes are intentionally NOT referenced from any template,
379
- // client script, menu or API payload. They do not appear in the admin
380
- // chats UI, so a "regular" administrator who has not read this source
381
- // will never encounter them. They are still protected by
382
- // assertAdminChatsAccess(), so only privileged accounts can read data
383
- // even if the URL is discovered. Paths:
384
- // GET /api/admin-chats/history/:mid -> JSON
385
- // GET /admin-chats/history/:mid -> standalone HTML page
376
+ // --- Chat edit-history (message options menu) -------------------------
377
+ // Surfaced as an "Edit history" item in each message's options dropdown
378
+ // (templates/partials/chats/message.tpl + static/lib/message-history.js),
379
+ // mirroring core's post diffs modal. Both routes require the manage
380
+ // privilege — view-only admin-chats access is not enough.
381
+ // GET /api/admin-chats/history/:mid -> JSON
382
+ // DELETE /api/admin-chats/history/:mid/:index -> delete one revision
386
383
  router.get('/api/admin-chats/history/:mid', ...routeMiddleware, async (req, res) => {
387
384
  try {
388
- if (!await assertAdminChatsAccess(req, res)) {
389
- return;
385
+ if (!req.uid || !await canManageAdminChats(req.uid)) {
386
+ return res.status(403).json({ status: { code: 'forbidden', message: 'Manage privilege required' } });
390
387
  }
391
388
 
392
389
  const mid = parseInt(req.params.mid, 10);
@@ -401,21 +398,61 @@ function registerRoutes(app, router, middleware) {
401
398
  }
402
399
  });
403
400
 
404
- router.get('/admin-chats/history/:mid', ...routeMiddleware, async (req, res) => {
401
+ // Restore a revision — like core post diffs (src/posts/diffs.js Diffs.restore),
402
+ // restoring is just an edit that sets the message content back to the
403
+ // revision's content. Going through Messaging.editMessage means the
404
+ // replaced current version is snapshotted into history (lib/overrides.js)
405
+ // and open chat windows update live via core's edit broadcast.
406
+ router.put('/api/admin-chats/history/:mid/:index', ...routeMiddleware, async (req, res) => {
405
407
  try {
406
- if (!await assertAdminChatsAccess(req, res)) {
407
- return;
408
+ if (!req.uid || !await canManageAdminChats(req.uid)) {
409
+ return res.status(403).json({ status: { code: 'forbidden', message: 'Manage privilege required' } });
408
410
  }
409
411
 
410
412
  const mid = parseInt(req.params.mid, 10);
411
- if (Number.isNaN(mid)) {
412
- return res.status(400).type('html').send('<h1>400</h1><p>Invalid message id</p>');
413
+ const index = parseInt(req.params.index, 10);
414
+ if (Number.isNaN(mid) || Number.isNaN(index) || index < 0) {
415
+ return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid message id or revision index' } });
416
+ }
417
+
418
+ const history = await getMessageEditHistory(mid);
419
+ if (!history.exists) {
420
+ return res.status(404).json({ status: { code: 'not-found', message: 'Message not found' } });
421
+ }
422
+ if (index >= history.revisions.length) {
423
+ return res.status(404).json({ status: { code: 'not-found', message: 'Revision not found' } });
424
+ }
425
+
426
+ await Messaging.editMessage(req.uid, mid, history.roomId, history.revisions[index].content);
427
+
428
+ const fresh = await getMessageEditHistory(mid);
429
+ return res.json({ status: { code: 'ok', message: 'Revision restored' }, ...fresh });
430
+ } catch (err) {
431
+ return res.status(500).json({ status: { code: 'error', message: err.message } });
432
+ }
433
+ });
434
+
435
+ router.delete('/api/admin-chats/history/:mid/:index', ...routeMiddleware, async (req, res) => {
436
+ try {
437
+ if (!req.uid || !await canManageAdminChats(req.uid)) {
438
+ return res.status(403).json({ status: { code: 'forbidden', message: 'Manage privilege required' } });
439
+ }
440
+
441
+ const mid = parseInt(req.params.mid, 10);
442
+ const index = parseInt(req.params.index, 10);
443
+ if (Number.isNaN(mid) || Number.isNaN(index) || index < 0) {
444
+ return res.status(400).json({ status: { code: 'bad-request', message: 'Invalid message id or revision index' } });
445
+ }
446
+
447
+ const deleted = await deleteRevision(mid, index);
448
+ if (!deleted) {
449
+ return res.status(404).json({ status: { code: 'not-found', message: 'Revision not found' } });
413
450
  }
414
451
 
415
452
  const history = await getMessageEditHistory(mid);
416
- return res.type('html').send(renderMessageHistoryHtml(history));
453
+ return res.json({ status: { code: 'ok', message: 'Revision deleted' }, ...history });
417
454
  } catch (err) {
418
- return res.status(500).type('html').send(`<h1>500</h1><pre>${validator.escape(String(err.message))}</pre>`);
455
+ return res.status(500).json({ status: { code: 'error', message: err.message } });
419
456
  }
420
457
  });
421
458
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-admin-chats",
3
- "version": "3.5.1",
3
+ "version": "3.6.0",
4
4
  "description": "A plugin that allows the administrator to view and manage user chats.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {
package/plugin.json CHANGED
@@ -9,7 +9,8 @@
9
9
  "static/lib/switch-chat.js",
10
10
  "static/lib/lock-toggle.js",
11
11
  "static/lib/live-updates.js",
12
- "static/lib/profile-link.js"
12
+ "static/lib/profile-link.js",
13
+ "static/lib/message-history.js"
13
14
  ],
14
15
  "hooks": [
15
16
  {
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "لا يمكن تنفيذ هذا الإجراء في غرفة مقفلة.",
9
9
  "admin-chats-view": "عرض جميع الدردشات",
10
10
  "admin-chats-manage": "إدارة غرف الدردشة",
11
- "participants": "المشاركون"
11
+ "participants": "المشاركون",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Diese Aktion kann in einem gesperrten Raum nicht ausgeführt werden.",
9
9
  "admin-chats-view": "Alle Chats ansehen",
10
10
  "admin-chats-manage": "Chat-Räume verwalten",
11
- "participants": "Teilnehmer"
11
+ "participants": "Teilnehmer",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Diese Aktion kann in einem gesperrten Raum nicht ausgeführt werden.",
9
9
  "admin-chats-view": "Alle Chats ansehen",
10
10
  "admin-chats-manage": "Chat-Räume verwalten",
11
- "participants": "Teilnehmer"
11
+ "participants": "Teilnehmer",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Αυτή η ενέργεια δεν μπορεί να εκτελεστεί σε κλειδωμένο δωμάτιο.",
9
9
  "admin-chats-view": "Προβολή όλων των συνομιλιών",
10
10
  "admin-chats-manage": "Διαχείριση δωματίων συνομιλίας",
11
- "participants": "Συμμετέχοντες"
11
+ "participants": "Συμμετέχοντες",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "This action cannot be performed in a locked room.",
9
9
  "admin-chats-view": "View all chats",
10
10
  "admin-chats-manage": "Manage chat rooms",
11
- "participants": "Participants"
11
+ "participants": "Participants",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "This action cannot be performed in a locked room.",
9
9
  "admin-chats-view": "View all chats",
10
10
  "admin-chats-manage": "Manage chat rooms",
11
- "participants": "Participants"
11
+ "participants": "Participants",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Esta acción no se puede realizar en una sala bloqueada.",
9
9
  "admin-chats-view": "Ver todos los chats",
10
10
  "admin-chats-manage": "Administrar salas de chat",
11
- "participants": "Participantes"
11
+ "participants": "Participantes",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Cette action ne peut pas être effectuée dans un salon verrouillé.",
9
9
  "admin-chats-view": "Voir tous les Chats",
10
10
  "admin-chats-manage": "Gérer les salons de discussion",
11
- "participants": "Participants"
11
+ "participants": "Participants",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Cette action ne peut pas être effectuée dans une salle verrouillée.",
9
9
  "admin-chats-view": "Voir toutes les discussions",
10
10
  "admin-chats-manage": "Gérer les salons de discussion",
11
- "participants": "Participants"
11
+ "participants": "Participants",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "לא ניתן לבצע פעולה זו בחדר נעול",
9
9
  "admin-chats-view": "צפיה בכל הצ'אטים",
10
10
  "admin-chats-manage": "ניהול חדרי צ'אט",
11
- "participants": "משתתפים"
11
+ "participants": "משתתפים",
12
+ "history.title": "היסטוריית עריכות",
13
+ "history.current": "גרסה נוכחית",
14
+ "history.revisionCount": "%1 עריכות קודמות",
15
+ "history.deleteRevision": "מחיקת גרסה",
16
+ "history.confirmDelete": "האם למחוק גרסה זו לצמיתות?",
17
+ "history.deleted": "הגרסה נמחקה",
18
+ "errors.history": "לא ניתן לטעון או לעדכן את היסטוריית העריכות.",
19
+ "history.restore": "שחזור גרסה",
20
+ "history.restored": "ההודעה שוחזרה לגרסה שנבחרה",
21
+ "history.restoreDescription": "שחזור יציב את תוכן הגרסה שנבחרה כגרסה הנוכחית; הגרסה שהוחלפה תישמר בהיסטוריה."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "लॉक किए गए कमरे में यह कार्रवाई नहीं की जा सकती।",
9
9
  "admin-chats-view": "सभी चैट देखें",
10
10
  "admin-chats-manage": "चैट रूम प्रबंधित करें",
11
- "participants": "प्रतिभागी"
11
+ "participants": "प्रतिभागी",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Questa azione non può essere eseguita in una stanza bloccata.",
9
9
  "admin-chats-view": "Visualizza tutte le chat",
10
10
  "admin-chats-manage": "Gestisci le stanze di chat",
11
- "participants": "Partecipanti"
11
+ "participants": "Partecipanti",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Questa azione non può essere eseguita in una stanza bloccata.",
9
9
  "admin-chats-view": "Visualizza tutte le chat",
10
10
  "admin-chats-manage": "Gestisci le stanze di chat",
11
- "participants": "Partecipanti"
11
+ "participants": "Partecipanti",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "ロックされたルームではこの操作は実行できません。",
9
9
  "admin-chats-view": "すべてのチャットを見る",
10
10
  "admin-chats-manage": "チャットルームを管理",
11
- "participants": "参加者"
11
+ "participants": "参加者",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Deze actie kan niet worden uitgevoerd in een vergrendelde chatruimte.",
9
9
  "admin-chats-view": "Alle chats bekijken",
10
10
  "admin-chats-manage": "Chatruimtes beheren",
11
- "participants": "Deelnemers"
11
+ "participants": "Deelnemers",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Tej czynności nie można wykonać w zablokowanym pokoju.",
9
9
  "admin-chats-view": "Wyświetl wszystkie czaty",
10
10
  "admin-chats-manage": "Zarządzaj pokojami czatu",
11
- "participants": "Uczestnicy"
11
+ "participants": "Uczestnicy",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Esta ação não pode ser realizada em uma sala bloqueada.",
9
9
  "admin-chats-view": "Ver todos os chats",
10
10
  "admin-chats-manage": "Gerenciar salas de chat",
11
- "participants": "Participantes"
11
+ "participants": "Participantes",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Questa acziun na po betg vegnir fatga en in local bloccà.",
9
9
  "admin-chats-view": "Veser tut ils chats",
10
10
  "admin-chats-manage": "Administrar locals da chat",
11
- "participants": "Participonts"
11
+ "participants": "Participonts",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Это действие нельзя выполнить в заблокированной комнате.",
9
9
  "admin-chats-view": "Просмотреть все чаты",
10
10
  "admin-chats-manage": "Управлять чат-комнатами",
11
- "participants": "Участники"
11
+ "participants": "Участники",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "Цю дію не можна виконати в блокованій кімнаті.",
9
9
  "admin-chats-view": "Переглянути всі чати",
10
10
  "admin-chats-manage": "Керувати чат-кімнатами",
11
- "participants": "Учасники"
11
+ "participants": "Учасники",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "锁定的聊天室无法执行此操作。",
9
9
  "admin-chats-view": "查看所有聊天",
10
10
  "admin-chats-manage": "管理聊天室",
11
- "participants": "参与者"
11
+ "participants": "参与者",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -8,5 +8,15 @@
8
8
  "errors.lockedAction": "鎖定的聊天室無法執行此操作。",
9
9
  "admin-chats-view": "查看所有聊天",
10
10
  "admin-chats-manage": "管理聊天室",
11
- "participants": "參與者"
11
+ "participants": "參與者",
12
+ "history.title": "Edit History",
13
+ "history.current": "Current version",
14
+ "history.revisionCount": "%1 previous edits",
15
+ "history.deleteRevision": "Delete version",
16
+ "history.confirmDelete": "Permanently delete this version?",
17
+ "history.deleted": "Version deleted",
18
+ "errors.history": "Unable to load or update the edit history.",
19
+ "history.restore": "Restore version",
20
+ "history.restored": "Message restored to the selected version",
21
+ "history.restoreDescription": "Restoring sets the selected version as the current one; the replaced version is kept in the history."
12
22
  }
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ // "Edit history" item in the chat message options dropdown (rendered
4
+ // server-side in templates/partials/chats/message.tpl, gated on manage
5
+ // privilege via isAdminOrGlobalMod). Opens a modal modelled on core's post
6
+ // diffs modal (public/src/client/topic/diffs.js): a revision selector, the
7
+ // selected revision's content, and a delete-revision button. The API routes
8
+ // it calls are manage-only as well, so the client gating is cosmetic.
9
+ $(document).ready(function () {
10
+ const localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' };
11
+
12
+ function prettyDate(timestamp) {
13
+ const numeric = parseInt(timestamp, 10) || 0;
14
+ if (!numeric) {
15
+ return '—';
16
+ }
17
+ return new Date(numeric).toLocaleString((config.userLang || 'en-GB').replace('_', '-'), localeStringOpts);
18
+ }
19
+
20
+ async function requestHistory(mid, method, revisionIndex) {
21
+ const suffix = (method === 'DELETE' || method === 'PUT') ? `/${revisionIndex}` : '';
22
+ const response = await fetch(`${config.relative_path || ''}/api/admin-chats/history/${mid}${suffix}`, {
23
+ method: method || 'GET',
24
+ headers: { 'x-csrf-token': config.csrf_token },
25
+ credentials: 'same-origin',
26
+ });
27
+
28
+ const data = await response.json();
29
+ if (!response.ok) {
30
+ throw new Error((data && data.status && data.status.message) || 'Request failed');
31
+ }
32
+
33
+ return data;
34
+ }
35
+
36
+ // The selector lists the current version first, then prior revisions
37
+ // newest-first. Option values keep the revision's index in the stored
38
+ // (oldest-first) list so deletes address the right entry.
39
+ function buildEntries(history, labels) {
40
+ const currentEditor = (history.current.editor && history.current.editor.username) ||
41
+ (history.author && history.author.username) || '';
42
+ const entries = [{
43
+ value: 'current',
44
+ label: `${prettyDate(history.current.edited || history.current.timestamp)} [${currentEditor}] · ${labels.current}`,
45
+ content: history.current.content,
46
+ }];
47
+
48
+ history.revisions.forEach((revision, index) => {
49
+ entries.push({
50
+ value: String(index),
51
+ label: `${prettyDate(revision.replacedAt)} [${revision.editor.username}]`,
52
+ content: revision.content,
53
+ });
54
+ });
55
+
56
+ return [entries[0]].concat(entries.slice(1).reverse());
57
+ }
58
+
59
+ function renderState($modal, history, labels) {
60
+ const $select = $modal.find('select');
61
+ const $content = $modal.find('.admin-chat-history-content');
62
+ const $deleteBtn = $modal.find('[data-action="delete-revision"]');
63
+ const $restoreBtn = $modal.find('[data-action="restore-revision"]');
64
+ const $count = $modal.find('.admin-chat-history-count');
65
+
66
+ const entries = buildEntries(history, labels);
67
+ const contentByValue = new Map(entries.map(entry => [entry.value, entry.content]));
68
+
69
+ $select.empty();
70
+ entries.forEach((entry) => {
71
+ const option = document.createElement('option');
72
+ option.value = entry.value;
73
+ option.textContent = entry.label;
74
+ $select.append(option);
75
+ });
76
+
77
+ $count.text(labels.count.replace('%1', String(history.revisionCount)));
78
+
79
+ const showSelection = () => {
80
+ const value = $select.val();
81
+ $content.text(contentByValue.get(value) || '');
82
+ $deleteBtn.prop('disabled', value === 'current');
83
+ $restoreBtn.prop('disabled', value === 'current');
84
+ };
85
+
86
+ $select.off('change').on('change', showSelection);
87
+ showSelection();
88
+ }
89
+
90
+ async function openHistoryModal(mid, modals, alerts, translator) {
91
+ const history = await requestHistory(mid);
92
+
93
+ const labels = {
94
+ current: await translator.translate('[[admin-chats:history.current]]'),
95
+ count: await translator.translate('[[admin-chats:history.revisionCount, %1]]'),
96
+ };
97
+
98
+ // modals.dialog runs the message through the translator itself
99
+ const $modal = await modals.dialog({
100
+ title: '[[admin-chats:history.title]]',
101
+ message: '<p class="admin-chat-history-count text-muted"></p>' +
102
+ '<div class="mb-3">' +
103
+ '<select class="form-control"></select>' +
104
+ '<hr />' +
105
+ '<pre class="admin-chat-history-content text-break mb-3" style="white-space: pre-wrap;"></pre>' +
106
+ '<button class="btn btn-primary" data-action="restore-revision">[[admin-chats:history.restore]]</button> ' +
107
+ '<button class="btn btn-danger" data-action="delete-revision">[[admin-chats:history.deleteRevision]]</button>' +
108
+ '<p class="form-text">[[admin-chats:history.restoreDescription]]</p>' +
109
+ '</div>',
110
+ size: 'large',
111
+ onEscape: true,
112
+ backdrop: true,
113
+ });
114
+
115
+ renderState($modal, history, labels);
116
+
117
+ $modal.find('[data-action="restore-revision"]').on('click', async function () {
118
+ const value = $modal.find('select').val();
119
+ if (value === 'current') {
120
+ return;
121
+ }
122
+
123
+ try {
124
+ await requestHistory(mid, 'PUT', value);
125
+ $modal.modal('hide');
126
+ alerts.success('[[admin-chats:history.restored]]');
127
+ } catch (err) {
128
+ alerts.error('[[admin-chats:errors.history]]');
129
+ }
130
+ });
131
+
132
+ $modal.find('[data-action="delete-revision"]').on('click', function () {
133
+ const value = $modal.find('select').val();
134
+ if (value === 'current') {
135
+ return;
136
+ }
137
+
138
+ modals.confirm('[[admin-chats:history.confirmDelete]]', async (confirmed) => {
139
+ if (!confirmed) {
140
+ return;
141
+ }
142
+
143
+ try {
144
+ const fresh = await requestHistory(mid, 'DELETE', value);
145
+ renderState($modal, fresh, labels);
146
+ alerts.success('[[admin-chats:history.deleted]]');
147
+ } catch (err) {
148
+ alerts.error('[[admin-chats:errors.history]]');
149
+ }
150
+ });
151
+ });
152
+ }
153
+
154
+ // Messages rendered live on the client (incoming messages, and a message
155
+ // re-parsed right after an edit) go through core's parseMessage, which
156
+ // sets isAdminOrGlobalMod = app.user.isAdmin || app.user.isGlobalMod —
157
+ // ignoring the plugin's manage privilege that gates the server-rendered
158
+ // item. Re-add the item on dropdown open when the room payload grants
159
+ // management and the item is missing.
160
+ $(document).on('show.bs.dropdown', '[component="chat/message"] .btn-group', function () {
161
+ if (!ajaxify.data || !ajaxify.data.isAdminOrGlobalMod) {
162
+ return;
163
+ }
164
+
165
+ const $menu = $(this).find('.dropdown-menu');
166
+ if (!$menu.length || $menu.find('.admin-chat-history-item').length) {
167
+ return;
168
+ }
169
+
170
+ const $message = $(this).closest('[component="chat/message"]');
171
+ const mid = parseInt($message.attr('data-mid'), 10);
172
+ if (!mid) {
173
+ return;
174
+ }
175
+
176
+ // Same gate as the server-rendered item: only edited messages have a
177
+ // history to show. The edited indicator element exists on every
178
+ // message and carries `hidden` while the message is unedited.
179
+ const $edited = $message.find('[component="chat/message/edited"]');
180
+ if (!$edited.length || $edited.hasClass('hidden')) {
181
+ return;
182
+ }
183
+
184
+ require(['translator'], function (translator) {
185
+ translator.translate('[[admin-chats:history.title]]', function (title) {
186
+ if ($menu.find('.admin-chat-history-item').length) {
187
+ return;
188
+ }
189
+
190
+ const $item = $(
191
+ '<li><a href="#" class="dropdown-item rounded-1 admin-chat-history-item" role="menuitem">' +
192
+ '<span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-history text-muted"></i> </span></a></li>'
193
+ );
194
+ $item.find('a').attr('data-mid', String(mid));
195
+ $item.find('span').append(document.createTextNode(title));
196
+
197
+ const $copyText = $menu.find('[data-action="copy-text"]').closest('li');
198
+ if ($copyText.length) {
199
+ $item.insertBefore($copyText);
200
+ } else {
201
+ $menu.append($item);
202
+ }
203
+ });
204
+ });
205
+ });
206
+
207
+ $(document).on('click', '.admin-chat-history-item', function (ev) {
208
+ ev.preventDefault();
209
+
210
+ const mid = parseInt($(this).attr('data-mid'), 10);
211
+ if (!mid) {
212
+ return;
213
+ }
214
+
215
+ require(['modals', 'alerts', 'translator'], function (modals, alerts, translator) {
216
+ openHistoryModal(mid, modals, alerts, translator).catch(() => {
217
+ alerts.error('[[admin-chats:errors.history]]');
218
+ });
219
+ });
220
+ });
221
+ });
@@ -55,6 +55,11 @@
55
55
  {{{ end }}}
56
56
 
57
57
  {{{ if isAdminOrGlobalMod }}}
58
+ {{{ if messages.edited }}}
59
+ <li>
60
+ <a href="#" class="dropdown-item rounded-1 admin-chat-history-item" data-mid="{messages.messageId}" role="menuitem"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-history text-muted"></i> {{tx("admin-chats:history.title")}}</span></a>
61
+ </li>
62
+ {{{ end }}}
58
63
  <li>
59
64
  <a href="#" class="dropdown-item rounded-1 chat-ip-button" role="menuitem">
60
65
  <span class="d-inline-flex align-items-center gap-2 show"><i class="fa fa-fw fa-info-circle text-muted"></i> {{tx("modules:chat.show-ip")}}</span>