Files
Haven/test/officialMessageOptions.test.js
Amnibro c8659a1cc7 Official messages: keep official mode on, and choose how they look (#5758)
All opt-in; nothing changes until someone picks a setting.

- Keep official mode on (Settings > Chat, saved on the account as
  users.official_mode, every device follows through set-official-mode /
  official-mode-changed). The megaphone then rests armed, so staff don't
  re-arm it for every message; pressing it sends just the next message
  plain. The client still sends highlight: true per message, so the wire
  format and the server's checks are unchanged.
- Official messages look (Settings > Chat, per device): Badge (the default,
  as before), Colored text (the text and a lighter wash in the role's color,
  a thinner bar, no badge; text mixed with the theme's own text color, more
  of it on light themes), or Off.
- With Colored text, consecutive official messages from one person group
  into one block. Badge keeps every official message's own header.
- session-info carries features: ['highlight_messages', 'official_mode'] and
  officialMode, so apps can tell what a server supports.
2026-10-12 17:14:57 -04:00

164 lines
7.2 KiB
JavaScript

'use strict';
// Official messages, the opt-in extras: keep official mode on (saved on the
// account, every device follows), how official messages look to the reader
// (Badge, Colored text, Off), runs of official messages grouping into one
// block, and the session-info features list.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const { pathToFileURL } = require('node:url');
const Database = require('better-sqlite3');
const highlights = require('../src/messageHighlights');
const register = require('../src/socketHandlers/messageHighlights');
const read = (p) => fs.readFileSync(path.join(__dirname, '..', p), 'utf8');
function freshDb() {
const db = new Database(':memory:');
db.exec(`
CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);
CREATE TABLE roles (id INTEGER PRIMARY KEY, name TEXT, color TEXT, color2 TEXT, color_shimmer INTEGER);
CREATE TABLE messages (id INTEGER PRIMARY KEY, user_id INTEGER);
INSERT INTO users (id, username) VALUES (1, 'alice'), (2, 'bob');
`);
highlights.ensureTable(db);
return db;
}
// Two sockets for alice (two devices) and one for bob, all on one io.
function fakeServer() {
const db = freshDb();
const sockets = new Map();
const io = { sockets: { sockets }, to: () => ({ emit: () => {} }) };
const connect = (id, user) => {
const handlers = new Map();
const sent = [];
const socket = { user, on: (ev, fn) => handlers.set(ev, fn), emit: (ev, data) => sent.push([ev, data]) };
sockets.set(id, socket);
register(socket, { io, db, userHasPermission: () => false, getUserHighestRole: () => null });
return { handlers, sent };
};
return {
db,
phone: connect('a1', { id: 1, isAdmin: false }),
desktop: connect('a2', { id: 1, isAdmin: false }),
bob: connect('b1', { id: 2, isAdmin: false }),
};
}
test('official mode is off until the person turns it on', () => {
const db = freshDb();
assert.equal(highlights.officialMode(db, 1), false);
assert.equal(highlights.setOfficialMode(db, 1, true), true);
assert.equal(highlights.officialMode(db, 1), true);
assert.equal(highlights.officialMode(db, 2), false, 'only their own account');
assert.equal(highlights.officialMode(db, 99), false, 'no such user');
// Running the migration again keeps what was saved.
highlights.ensureTable(db);
assert.equal(highlights.officialMode(db, 1), true);
});
test('switching official mode reaches every device of theirs and nobody else', () => {
const { db, phone, desktop, bob } = fakeServer();
let reply = null;
phone.handlers.get('set-official-mode')({ enabled: true }, (r) => { reply = r; });
assert.deepEqual(reply, { ok: true, officialMode: true });
assert.equal(highlights.officialMode(db, 1), true);
assert.deepEqual(phone.sent, [['official-mode-changed', { officialMode: true }]]);
assert.deepEqual(desktop.sent, [['official-mode-changed', { officialMode: true }]]);
assert.deepEqual(bob.sent, []);
assert.equal(highlights.officialMode(db, 2), false);
desktop.handlers.get('set-official-mode')({ enabled: false });
assert.equal(highlights.officialMode(db, 1), false, 'no callback is fine');
assert.deepEqual(phone.sent.at(-1), ['official-mode-changed', { officialMode: false }]);
});
test('a malformed official mode request changes nothing', () => {
const { db, phone } = fakeServer();
for (const bad of [null, 'on', { enabled: 'true' }, { enabled: 1 }, {}]) {
let reply = null;
phone.handlers.get('set-official-mode')(bad, (r) => { reply = r; });
assert.equal(reply.ok, false);
}
assert.equal(highlights.officialMode(db, 1), false);
assert.deepEqual(phone.sent, []);
});
test('session-info tells clients what the server supports and the sender\'s mode', () => {
assert.deepEqual(highlights.FEATURES, ['highlight_messages', 'official_mode']);
const src = read('src/socketHandlers/index.js');
assert.match(src, /officialMode: highlights\.officialMode\(db, socket\.user\.id\),/);
assert.match(src, /features: highlights\.FEATURES/);
});
let M;
test.before(async () => {
const modulePath = path.join(__dirname, '..', 'public/js/modules/app-message-highlights.js');
M = (await import(pathToFileURL(modulePath).href)).default;
});
function withStyle(style, fn) {
const store = new Map(style ? [['haven_official_style', style]] : []);
const prev = global.localStorage;
global.localStorage = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)) };
try { return fn(); } finally { global.localStorage = prev; }
}
test('the badge stays the default look, and unknown values fall back to it', () => {
assert.equal(withStyle(null, () => M._officialStyle()), 'badge');
assert.equal(withStyle('text', () => M._officialStyle()), 'text');
assert.equal(withStyle('off', () => M._officialStyle()), 'off');
assert.equal(withStyle('sparkles', () => M._officialStyle()), 'badge');
});
test('official runs group only with Colored text; the badge keeps every header', () => {
const official = { highlight: { role: 'Mod' } };
const plain = {};
// Badge (the default): an official message never groups, either way.
withStyle(null, () => {
assert.equal(M._highlightGroups(official, official), false);
assert.equal(M._highlightGroups(plain, official), false);
assert.equal(M._highlightGroups(official, plain), false);
assert.equal(M._highlightGroups(plain, plain), true);
});
// Colored text: one block per run, and plain and official do not mix.
withStyle('text', () => {
assert.equal(M._highlightGroups(official, official), true);
assert.equal(M._highlightGroups(plain, official), false);
assert.equal(M._highlightGroups(official, plain), false);
assert.equal(M._highlightGroups(plain, plain), true);
});
// Off: the mark makes no difference.
withStyle('off', () => {
assert.equal(M._highlightGroups(official, plain), true);
assert.equal(M._highlightGroups(official, official), true);
});
});
test('with official mode on the button rests armed, and only where it shows', () => {
const app = Object.assign(Object.create(M), { user: { officialMode: true }, _highlightAvailable: true });
assert.equal(app._highlightDefault(), true);
app._highlightAvailable = false; // a DM, or no permission here
assert.equal(app._highlightDefault(), false);
app._highlightAvailable = true;
app.user.officialMode = false;
assert.equal(app._highlightDefault(), false, 'off: one message per press, as before');
});
test('the composer and the menus are wired to the settings', () => {
const messages = read('public/js/modules/app-messages.js');
assert.match(messages, /if \(this\._highlightArmed\) payload\.highlight = true;\n\s*this\._setHighlightArmed\(this\._highlightDefault\(\)\);/);
assert.match(messages, /this\._highlightGroups\(msg, prevMsg\) &&/);
const html = read('public/app.html');
assert.match(html, /<select id="official-style-select"[^>]*>\s*<option value="badge"/);
assert.match(html, /<div id="official-mode-setting" style="display:none">/);
assert.match(html, /<input type="checkbox" id="official-mode-toggle">/);
const css = read('public/css/style-messages.css');
assert.match(css, /:root\[data-official-style\] \.msg-highlight-badge \{ display: none; \}/);
});