fix(app): isolate bidi usernames from message timestamps (#2560)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-06 22:41:57 +02:00
1 parent 6c0ce9369b
commit f38619d974
2 files changed
+62

No files matched your search

@@ -428,6 +428,7 @@
max-inline-size: none; max-inline-size: none;
overflow: visible; overflow: visible;
white-space: nowrap; white-space: nowrap;
unicode-bidi: plaintext;
user-select: text; user-select: text;
-webkit-user-select: text; -webkit-user-select: text;
} }
@@ -505,6 +506,7 @@
font-weight: 400; font-weight: 400;
color: var(--message-timestamp-color); color: var(--message-timestamp-color);
line-height: var(--message-line-height); line-height: var(--message-line-height);
unicode-bidi: isolate;
} }
.messageTimestamp { .messageTimestamp {
@@ -557,6 +559,7 @@
font-size: var(--message-timestamp-compact-font-size); font-size: var(--message-timestamp-compact-font-size);
font-weight: 500; font-weight: 500;
line-height: var(--message-line-height); line-height: var(--message-line-height);
unicode-bidi: isolate;
} }
.messageTimestampHover { .messageTimestampHover {
@@ -948,6 +951,7 @@
white-space: nowrap; white-space: nowrap;
max-width: 30%; max-width: 30%;
vertical-align: baseline; vertical-align: baseline;
unicode-bidi: plaintext;
font-weight: 500; font-weight: 500;
color: color-mix( color: color-mix(
in srgb, in srgb,
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {readFileSync} from 'node:fs';
import {fileURLToPath} from 'node:url';
import {describe, expect, it} from 'vitest';
const MESSAGE_CSS = readFileSync(fileURLToPath(new URL('./Message.module.css', import.meta.url)), 'utf8');
const ISOLATED_NAMES = ['.messageUsername', '.repliedUsername'];
const ISOLATED_TIMESTAMPS = [
'.messageTimestamp',
'.messageTimestampCompact',
'.messageTimestampHover',
'.messageTimestampCompactHover',
];
function rules(css: string): Array<{selector: string; declarations: string}> {
const source = css.replace(/\/\*[\s\S]*?\*\//g, '');
const out: Array<{selector: string; declarations: string}> = [];
const chain: Array<string> = [];
let buffer = '';
for (const char of source) {
if (char === '{') {
chain.push(buffer.split(/[;}]/).pop()?.trim().replace(/\s+/g, ' ') ?? '');
buffer = '';
} else if (char === '}') {
if (chain.length > 0) {
out.push({selector: chain.join(' '), declarations: buffer});
chain.pop();
}
buffer = '';
} else {
buffer += char;
}
}
return out;
}
function declaredBidi(className: string): Array<string> {
const selectorPattern = new RegExp(`\\${className}(?![\\w-])`);
const out: Array<string> = [];
for (const rule of rules(MESSAGE_CSS)) {
if (!rule.selector.split(',').some((selector) => selectorPattern.test(selector))) continue;
const match = rule.declarations.match(/unicode-bidi\s*:\s*([^;]+)/);
if (match) out.push(match[1].trim());
}
return out;
}
describe('message bidi isolation', () => {
it.each(ISOLATED_NAMES)('resolves %s in its own bidi paragraph', (className) => {
expect(declaredBidi(className)).toContain('plaintext');
});
it.each(ISOLATED_TIMESTAMPS)('keeps %s out of a neighbouring right-to-left run', (className) => {
expect(declaredBidi(className)).toContain('isolate');
});
});