- ReadPositionsContext is provided once at Room level so the thread panel (a sibling of RoomView) gets real positions instead of the empty default; own thread messages no longer sit on "Sent" forever (#38). - Receipt events only recompute the users they name, merged into the previous map with reference equality preserved for untouched rows, so a receipt no longer re-renders every message (#40). Unit-tested. Fixes #38 Fixes #40 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import type { Room, MatrixEvent } from 'matrix-js-sdk';
|
|
import { ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts';
|
|
import { computeUpdatedPositions, getReceiptUserIds } from './useRoomReadPositions';
|
|
|
|
// Fake, renderable (no reaction/edit relation) event.
|
|
const fakeEvent = (id: string) =>
|
|
({
|
|
getId: () => id,
|
|
getRelation: () => null,
|
|
}) as unknown as MatrixEvent;
|
|
|
|
// Minimal fake room: a fixed live timeline plus a mutable per-user read-up-to map,
|
|
// which the test mutates between calls to simulate new receipts arriving.
|
|
const makeFakeRoom = (eventIds: string[]) => {
|
|
const readUpTo = new Map<string, string>();
|
|
const events = eventIds.map(fakeEvent);
|
|
const room = {
|
|
getLiveTimeline: () => ({ getEvents: () => events }),
|
|
getEventReadUpTo: (userId: string) => readUpTo.get(userId) ?? null,
|
|
} as unknown as Room;
|
|
return { room, readUpTo };
|
|
};
|
|
|
|
test('getReceiptUserIds collects every userId across events/types in the content', () => {
|
|
const content = {
|
|
$a: {
|
|
[ReceiptType.Read]: { '@alice:hs': { ts: 1 }, '@bob:hs': { ts: 2 } },
|
|
},
|
|
$b: {
|
|
[ReceiptType.ReadPrivate]: { '@carol:hs': { ts: 3 } },
|
|
},
|
|
};
|
|
const ids = getReceiptUserIds(content).sort();
|
|
assert.deepEqual(ids, ['@alice:hs', '@bob:hs', '@carol:hs']);
|
|
});
|
|
|
|
test('getReceiptUserIds returns empty for an empty receipt content', () => {
|
|
assert.deepEqual(getReceiptUserIds({}), []);
|
|
});
|
|
|
|
test('computeUpdatedPositions returns the SAME map reference when no named user changed position', () => {
|
|
const { room, readUpTo } = makeFakeRoom(['$a', '$b']);
|
|
readUpTo.set('@alice:hs', '$a');
|
|
const prev = computeUpdatedPositions(room, '@me:hs', new Map(), ['@alice:hs']);
|
|
assert.deepEqual(prev.get('$a'), ['@alice:hs']);
|
|
|
|
// Alice's read-up-to did not move; recompute should be a no-op reference-wise.
|
|
const next = computeUpdatedPositions(room, '@me:hs', prev, ['@alice:hs']);
|
|
assert.equal(next, prev);
|
|
});
|
|
|
|
test('computeUpdatedPositions moves only the named user, leaving other entries untouched by reference', () => {
|
|
const { room, readUpTo } = makeFakeRoom(['$a', '$b']);
|
|
readUpTo.set('@alice:hs', '$a');
|
|
readUpTo.set('@bob:hs', '$a');
|
|
const prev = computeUpdatedPositions(room, '@me:hs', new Map(), ['@alice:hs', '@bob:hs']);
|
|
const bArrayBefore = prev.get('$a');
|
|
|
|
// Only alice moves to $b.
|
|
readUpTo.set('@alice:hs', '$b');
|
|
const next = computeUpdatedPositions(room, '@me:hs', prev, ['@alice:hs']);
|
|
|
|
assert.notEqual(next, prev);
|
|
assert.deepEqual(next.get('$a'), ['@bob:hs']);
|
|
assert.deepEqual(next.get('$b'), ['@alice:hs']);
|
|
// Untouched arrays for other targets should keep their old reference where possible.
|
|
assert.notEqual(next.get('$a'), bArrayBefore); // this one WAS filtered, expected to change
|
|
});
|
|
|
|
test('computeUpdatedPositions ignores changedUserIds not present after filtering myUserId', () => {
|
|
const { room } = makeFakeRoom(['$a']);
|
|
const prev = new Map([['$a', ['@bob:hs']]]);
|
|
const next = computeUpdatedPositions(room, '@me:hs', prev, ['@me:hs']);
|
|
assert.equal(next, prev);
|
|
});
|
|
|
|
test('computeUpdatedPositions removes a user with no read-up-to event from the map', () => {
|
|
const { room, readUpTo } = makeFakeRoom(['$a']);
|
|
readUpTo.set('@alice:hs', '$a');
|
|
const prev = computeUpdatedPositions(room, '@me:hs', new Map(), ['@alice:hs']);
|
|
assert.deepEqual(prev.get('$a'), ['@alice:hs']);
|
|
|
|
readUpTo.delete('@alice:hs');
|
|
const next = computeUpdatedPositions(room, '@me:hs', prev, ['@alice:hs']);
|
|
assert.equal(next.has('$a'), false);
|
|
});
|