mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
Guild chat per guild (/chat_log/guild/<id>, from the guild's card), team chat by team (/chat_log/teams) and a character's whispers by conversation (/characters/<id>/whispers, chat_dms; opening one is audited), paged on the server. The Chat Log filters by account and time range, shows the conversation around a message, links to its history and marks flagged messages; messages can be picked (shift-click for a range) to flag. The character page links to all of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
7.0 KiB
JavaScript
137 lines
7.0 KiB
JavaScript
/**
|
|
* Picking chat messages and flagging them for review, shared by the Chat Log and the chat histories (guild, team and
|
|
* whisper conversations). Each message row has a checkbox (ChatFlagging.checkbox); shift-click picks every message
|
|
* between the last one clicked and this one. The bar (ChatFlagging.bar) shows how many are picked and opens the flag
|
|
* dialog: who the flag is about (one of the senders) and a note. The server keeps a copy of the messages and the chat
|
|
* around them. Messages already flagged link to their flag (ChatFlagging.flagLink).
|
|
*/
|
|
window.ChatFlagging = (function () {
|
|
'use strict';
|
|
|
|
var CHANNEL_NEEDS = { whisper: 'chat_dms', team: 'chat_private', guild: 'chat_private' };
|
|
|
|
function flagLink(row) {
|
|
if (!row.flag_id) return '';
|
|
return ' <a class="badge text-bg-warning text-decoration-none" href="/chat_flags#flag=' + esc(row.flag_id) + '" title="In chat flag ' + esc(row.flag_id) + '">Flagged</a>';
|
|
}
|
|
|
|
// The message text, or why it can't be shown
|
|
function text(row) {
|
|
if (row.redacted) return '<span class="text-body-secondary fst-italic">Hidden: reading ' + esc(row.channel) + ' chat needs ' + esc(CHANNEL_NEEDS[row.channel] || 'another permission') + '</span>';
|
|
var badges = '';
|
|
if (row.blocked) badges += fmt.badge('Stopped by the filter', 'danger') + ' ';
|
|
else if (row.filtered) badges += fmt.badge('Filter words', 'warning') + ' ';
|
|
return badges + '<span class="' + (row.blocked ? 'text-body-secondary' : '') + '">' + esc(row.message) + '</span>';
|
|
}
|
|
|
|
function checkbox(row) {
|
|
if (!DASH.can('chat_flag') || row.redacted || !row.id) return '';
|
|
return '<input class="form-check-input" type="checkbox" data-flag-pick="' + esc(row.id) + '" aria-label="Pick this message to flag">';
|
|
}
|
|
|
|
/**
|
|
* Picking in `container` (an element, or a selector), with the bar in `barEl`. onFlagged(id) runs after a flag
|
|
* is made.
|
|
*/
|
|
function create(container, barEl, onFlagged) {
|
|
var root = typeof container === 'string' ? document.querySelector(container) : container;
|
|
var bar = typeof barEl === 'string' ? document.querySelector(barEl) : barEl;
|
|
var rows = {}; // id -> message, for every message shown
|
|
var picked = {}; // id -> true
|
|
var lastClicked = null;
|
|
|
|
function count() { return Object.keys(picked).length; }
|
|
|
|
function renderBar() {
|
|
if (!bar) return;
|
|
var n = count();
|
|
bar.classList.toggle('d-none', !DASH.can('chat_flag'));
|
|
bar.innerHTML = '<span class="small text-body-secondary">' + (n ? n + ' message' + (n === 1 ? '' : 's') + ' picked' :
|
|
'Tick messages to flag them for review (shift-click picks a range).') + '</span>' +
|
|
(n ? ' <button type="button" class="btn btn-sm btn-warning" data-flag-open>Flag…</button> <button type="button" class="btn btn-sm btn-outline-secondary" data-flag-clear>Clear</button>' : '');
|
|
}
|
|
|
|
function sync() {
|
|
root.querySelectorAll('[data-flag-pick]').forEach(function (box) { box.checked = !!picked[box.getAttribute('data-flag-pick')]; });
|
|
renderBar();
|
|
}
|
|
|
|
function clear() { picked = {}; lastClicked = null; sync(); }
|
|
|
|
root.addEventListener('click', function (e) {
|
|
var box = e.target.closest('[data-flag-pick]');
|
|
if (!box) return;
|
|
var id = box.getAttribute('data-flag-pick');
|
|
var on = box.checked;
|
|
if (e.shiftKey && lastClicked !== null) {
|
|
var boxes = Array.prototype.slice.call(root.querySelectorAll('[data-flag-pick]'));
|
|
var ids = boxes.map(function (b) { return b.getAttribute('data-flag-pick'); });
|
|
var a = ids.indexOf(lastClicked), b = ids.indexOf(id);
|
|
if (a !== -1 && b !== -1) {
|
|
for (var i = Math.min(a, b); i <= Math.max(a, b); i++) {
|
|
if (on) picked[ids[i]] = true; else delete picked[ids[i]];
|
|
}
|
|
}
|
|
} else if (on) picked[id] = true;
|
|
else delete picked[id];
|
|
lastClicked = id;
|
|
sync();
|
|
});
|
|
|
|
if (bar) bar.addEventListener('click', function (e) {
|
|
if (e.target.closest('[data-flag-clear]')) return clear();
|
|
if (e.target.closest('[data-flag-open]')) openDialog();
|
|
});
|
|
|
|
function openDialog() {
|
|
var ids = Object.keys(picked);
|
|
var messages = ids.map(function (id) { return rows[id]; }).filter(Boolean).sort(function (a, b) { return a.id - b.id; });
|
|
var senders = [];
|
|
messages.forEach(function (m) {
|
|
if (m.sender_id !== '0' && !senders.some(function (s) { return s.id === m.sender_id; })) senders.push({ id: m.sender_id, name: m.sender_name });
|
|
});
|
|
var modalEl = document.createElement('div');
|
|
modalEl.className = 'modal fade';
|
|
modalEl.tabIndex = -1;
|
|
modalEl.innerHTML = '<div class="modal-dialog modal-lg"><form class="modal-content">' +
|
|
'<div class="modal-header"><h5 class="modal-title">Flag ' + ids.length + ' message' + (ids.length === 1 ? '' : 's') + '</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div>' +
|
|
'<div class="modal-body">' +
|
|
'<div class="border rounded p-2 mb-3 small" style="max-height: 14rem; overflow-y: auto">' + messages.map(function (m) {
|
|
return '<div><span class="text-body-secondary">' + esc(fmt.unix(m.time)) + '</span> <strong>' + esc(m.sender_name) + '</strong>' +
|
|
(m.recipient_name ? ' → ' + esc(m.recipient_name) : '') + ': ' + esc(m.message) + '</div>';
|
|
}).join('') + '</div>' +
|
|
(senders.length ? '<div class="mb-3"><label class="form-label" for="flagAbout">About</label><select class="form-select" id="flagAbout">' +
|
|
senders.map(function (s) { return '<option value="' + esc(s.id) + '">' + esc(s.name) + '</option>'; }).join('') + '</select></div>' : '') +
|
|
'<div><label class="form-label" for="flagNote">Note</label><textarea class="form-control" id="flagNote" rows="3" maxlength="2000" placeholder="What is wrong here, for whoever reviews it"></textarea>' +
|
|
'<div class="form-text">The flag keeps a copy of these messages and the chat around them, even after the chat log is pruned. The messages must be from one conversation.</div></div>' +
|
|
'</div>' +
|
|
'<div class="modal-footer"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button><button type="submit" class="btn btn-warning">Flag</button></div>' +
|
|
'</form></div>';
|
|
document.body.appendChild(modalEl);
|
|
var modal = new bootstrap.Modal(modalEl);
|
|
modalEl.addEventListener('hidden.bs.modal', function () { modal.dispose(); modalEl.remove(); });
|
|
modalEl.querySelector('form').addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
var about = modalEl.querySelector('#flagAbout');
|
|
api.action('/api/chat/flags', { message_ids: ids, character: about ? about.value : '', note: modalEl.querySelector('#flagNote').value }, 'Flagged for review').then(function (d) {
|
|
modal.hide();
|
|
clear();
|
|
if (onFlagged) onFlagged(d.id);
|
|
}).catch(function () {});
|
|
});
|
|
modal.show();
|
|
}
|
|
|
|
renderBar();
|
|
return {
|
|
// Remember the messages shown (for the dialog); call before or after rendering them
|
|
remember: function (messages) { messages.forEach(function (m) { rows[m.id] = m; }); },
|
|
// After the rows were drawn again: tick what's picked
|
|
sync: sync,
|
|
clear: clear
|
|
};
|
|
}
|
|
|
|
return { create: create, checkbox: checkbox, flagLink: flagLink, text: text };
|
|
})();
|