Split client code from server

This commit is contained in:
2026-03-20 23:13:27 +01:00
parent eb3910d7ea
commit 1fc21d700e
117 changed files with 850 additions and 762 deletions
+2 -138
View File
@@ -1,15 +1,13 @@
import * as moment from 'moment';
import { escape, escapeRegExp, startsWith, range, uniq, compact } from 'lodash';
import { escapeRegExp, startsWith, range, uniq, compact } from 'lodash';
import { fromNow, toInt, hasFlag, compareDates, removeItem, includes } from './utils';
import { DAY } from './constants';
import {
Account, OriginInfo, Document, OriginRef, SupporterFlags, BannedMuted, AccountBase, LogEntry,
Account, OriginInfo, Document, OriginRef, SupporterFlags, BannedMuted, AccountBase,
DuplicateResult, DuplicateBase, Duplicate, Auth, MergeInfo
} from './adminInterfaces';
import { hasRole } from './accountUtils';
import { filterBadWordsPartial } from './swears';
import { faPlusCircle, faClock, faMinusCircle, faCaretSquareUp, faCaretSquareDown } from '../client/icons';
import { element, textNode } from '../client/htmlUtils';
interface UpdatedAt {
updatedAt: Date;
@@ -52,108 +50,6 @@ export function getAge(birthdate: Date) {
// chat & events
function enc(text?: string): string {
return escape(text || '');
}
function encWithHighlight(text?: string): string {
return highlightWords(enc(text || ''));
}
export function formatEventDesc(text: string): string {
return encWithHighlight(text).replace(/\[([a-z0-f]{24})\]/g, `<a tabindex onclick="goToAccount('$1')">[$1]</a>`);
}
function getMessageTag(message: string) {
if (/^\/p /.test(message)) {
return 'party';
} else if (/^\/w /.test(message)) {
return 'whisper';
} else if (/^\/s[s123] /.test(message)) {
return 'supporter';
} else if (/^\//.test(message)) {
return 'command';
} else {
return 'none';
}
}
export function replaceSwears(element: HTMLElement) {
const text = element.textContent;
if (text) {
const replaced = encWithHighlight(text);
if (text !== replaced) {
element.innerHTML = replaced;
}
}
}
function formatChatLine(l: string): HTMLElement {
// 00:00:01 [system] Timed out for swearing
// 00:00:01 [patreon] fetched patreon data
// 00:00:01 [dev][Autumn Leafs] hello world
// 00:00:01 [dev][Autumn Leafs][muted] hello world
// 00:00:01 [dev][Autumn Leafs][ignored] hello world
// 00:00:01 [dev-pl][Autumn Leafs][ignored] hello world
// 00:00:01 [57a3dc6f2f0019a161cdebf6][dev][Autumn Leafs][ignored] hello world
// 00:00:01 [1][dev][Autumn Leafs][ignored] hello world
// 00:00:01 [1:merged][dev][Autumn Leafs][ignored] hello world
// 00:00:01 [merged][dev][Autumn Leafs][ignored] hello world
// 00:00:01 [merged][dev][main][Autumn Leafs][ignored] hello world
/* tslint:disable:max-line-length */
const regex = /^([0-9:]+) (\[(?:merged|\d+|\d+:merged|[a-z0-9]{24})\])?\[([a-z0-9_-]+)\](?:\[([a-z0-9_-]+)\])?((?:\[.*?\])?)(?:\[(muted|ignored|ignorepub)\])?\t(.*)$/;
const m = regex.exec(l);
if (m) {
const [, time, accountId, server, map, name, mutedIgnored, message] = m;
const messageTag = server === 'system' ? 'system' : getMessageTag(message);
const modTag = mutedIgnored ? ' message-muted' : '';
return element('div', 'chatlog-line', [
element('span', 'time', [], { 'data-text': time }),
accountId ? element('span', 'account-id', [textNode(accountId)]) : undefined,
element('span', `server server-${server.replace(/-.+$/g, '')}`, [textNode(`[${server}]`)]),
map ? element('span', `map map-${map}`, [textNode(`[${map}]`)]) : undefined,
element('span', mutedIgnored ? `name ${mutedIgnored}` : `name`, [textNode(name)]),
textNode(' '),
element('span', `message message-${messageTag}${modTag}`, [textNode(message)]),
textNode(' '),
element('a', 'chat-translate', [], undefined, { click: translateChat }),
]);
} else {
return element('div', '', [textNode(highlightWords(l))]);
}
}
function translateChat(this: HTMLElement) {
const lines: string[] = [];
let parent = this.parentElement;
for (let i = 0; parent && i < 10; i++) {
lines.push(parent.querySelector('.message')!.textContent!);
parent = parent.nextElementSibling as HTMLElement;
}
window.open(`https://translate.google.com/#auto/en/${encodeURIComponent(lines.join('\n'))}`);
}
if (typeof window !== 'undefined') {
(window as any).goToAccount = (accountId: string) => {
window.dispatchEvent(new CustomEvent('go-to-account', { detail: accountId }));
};
}
export function formatChat(chat: string): HTMLElement[] {
return (chat || '<no messages>')
.trim()
.split(/\r?\n/g)
.reverse()
.map(formatChatLine);
}
export interface ChatDate {
value: string;
label: string;
@@ -483,38 +379,6 @@ export interface SupporterChange {
class: string;
}
export function createSupporterChanges(entries: LogEntry[]): SupporterChange[] {
const changes = entries.map(l => ({
message: l.message,
level: +((/\d+/.exec(l.message) || ['0'])[0]),
added: /added/i.test(l.message),
date: new Date(l.date),
icon: /added/i.test(l.message) ? faPlusCircle : (/decline/i.test(l.message) ? faClock : faMinusCircle),
class: /added/i.test(l.message) ? 'text-success' : (/decline/i.test(l.message) ? 'text-warning' : 'text-danger'),
}));
for (let i = 1; i < changes.length; i++) {
const prev = changes[i - 1];
const current = changes[i];
if (current.date.getMonth() !== prev.date.getMonth()) {
current.class += ' border-left border-success pl-2';
}
if (current.added && prev.added) {
if (current.level > prev.level) {
current.icon = faCaretSquareUp;
current.class = 'text-info';
} else if (current.level < prev.level) {
current.icon = faCaretSquareDown;
current.class = 'text-info';
}
}
}
return changes;
}
export function getIdsFromNote(note: string | undefined) {
return note ? uniq(note.match(/[0-9a-f]{24}/g)) : [];
}