Nothing ever removed an indexed row: redacted messages stayed searchable with full plaintext and rendered as normal results. Now: a client-level RoomEvent.Redaction listener deletes the row, leave/ban clears the room (clearRoom finally has a caller), m.replace edits upsert the original row instead of indexing the "* fallback" separately, and cached rows whose local event is redacted render through the existing redacted_because placeholder. Unit-tested. Fixes #14 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
57 lines
2.3 KiB
TypeScript
57 lines
2.3 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { SearchCacheRow } from '../../utils/searchCache';
|
|
|
|
// useLocalMessageSearch.ts imports searchCacheEnabledAtom, which touches
|
|
// localStorage at module-load time (atomWithLocalStorage reads the initial
|
|
// value eagerly). Stub it before a dynamic import — a static import would
|
|
// hoist above the stub. Same pattern as state/plaintextCaches.test.ts.
|
|
(globalThis as { localStorage?: unknown }).localStorage = {
|
|
getItem: () => null,
|
|
setItem: () => {},
|
|
removeItem: () => {},
|
|
};
|
|
|
|
const { rowToResultItem } = await import('./useLocalMessageSearch');
|
|
|
|
const row = (overrides: Partial<SearchCacheRow> = {}): SearchCacheRow => ({
|
|
roomId: '!r1',
|
|
eventId: '$1',
|
|
ts: 100,
|
|
sender: '@a',
|
|
body: 'hello world',
|
|
...overrides,
|
|
});
|
|
|
|
// Gitea #14 — cached rows for a locally-known-redacted event must carry a
|
|
// `redacted_because` marker so SearchResultGroup's guard renders the
|
|
// "message deleted" placeholder instead of the stale plaintext.
|
|
|
|
test('rowToResultItem: plain row has no redacted_because marker', () => {
|
|
const item = rowToResultItem(row());
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
assert.equal((item.event as any).unsigned?.redacted_because, undefined);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
assert.equal((item.event as any).content.body, 'hello world');
|
|
});
|
|
|
|
test('rowToResultItem: redacted=true sets the redacted_because marker', () => {
|
|
const item = rowToResultItem(row(), true);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
assert.ok((item.event as any).unsigned?.redacted_because);
|
|
});
|
|
|
|
test('rowToResultItem: falls back to pollText when body is empty', () => {
|
|
const item = rowToResultItem(row({ body: '', pollText: 'question answer' }));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
assert.equal((item.event as any).content.body, 'question answer');
|
|
});
|
|
|
|
test('rowToResultItem: carries formattedBody as HTML when present', () => {
|
|
const item = rowToResultItem(row({ formattedBody: '<b>hi</b>' }));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const content = (item.event as any).content;
|
|
assert.equal(content.format, 'org.matrix.custom.html');
|
|
assert.equal(content.formatted_body, '<b>hi</b>');
|
|
});
|