feat: reaction TDS styling, debounce read receipts, Escape to skip boot, type fixes

- lotus-terminal.css.ts: add reaction chip styles for dark + light TDS modes
  (cyan border/bg for unselected, orange accent for own/pressed reactions)
- useRoomReadPositions: debounce receipt handler at 150ms (M-3)
- lotus-boot.ts: Escape key skips boot animation (I-3)
- RoomInput.tsx: replace (uploadRes as any) with typed assertion (M-7)
- CallEmbedProvider: call mention detection, audio cleanup, display name (C-1, C-2, M-5)
- EventReaders: timestamps in seen-by modal, filter self, TDS styling
- ReadReceiptAvatars: StackedAvatar pill, TDS visual treatment
- chatBackground: add waves/neon/aurora backgrounds
- RoomView: auto-apply tactical bg when TDS active and bg is none
- settings: extend ChatBackground union type
This commit is contained in:
root
2026-05-16 01:34:20 -04:00
parent 1e5d5f3fe4
commit 4c4d61600d
10 changed files with 270 additions and 68 deletions
+28 -4
View File
@@ -1,16 +1,32 @@
import { Room, RoomEvent } from 'matrix-js-sdk';
import { Room, RoomEvent, MatrixEvent } from 'matrix-js-sdk';
import { useEffect, useState } from 'react';
import { useMatrixClient } from './useMatrixClient';
import { reactionOrEditEvent } from '../utils/room';
// Receipts can land on reaction/edit events which RoomTimeline skips (renders null).
// Walk backwards from the receipt event to find the nearest event that IS rendered.
function nearestRenderableId(liveEvents: MatrixEvent[], evtId: string): string | null {
const idx = liveEvents.findIndex(e => e.getId() === evtId);
if (idx === -1) return null;
for (let i = idx; i >= 0; i--) {
const e = liveEvents[i];
if (!reactionOrEditEvent(e)) return e.getId() ?? null;
}
return null;
}
function computePositions(room: Room, myUserId: string): Map<string, string[]> {
const map = new Map<string, string[]>();
const liveEvents = room.getLiveTimeline().getEvents();
for (const member of room.getJoinedMembers()) {
if (member.userId === myUserId) continue;
const evtId = room.getEventReadUpTo(member.userId);
if (!evtId) continue;
const arr = map.get(evtId);
const targetId = nearestRenderableId(liveEvents, evtId);
if (!targetId) continue;
const arr = map.get(targetId);
if (arr) arr.push(member.userId);
else map.set(evtId, [member.userId]);
else map.set(targetId, [member.userId]);
}
return map;
}
@@ -22,9 +38,17 @@ export function useRoomReadPositions(room: Room): Map<string, string[]> {
useEffect(() => {
setPositions(computePositions(room, myUserId));
const onReceipt = () => setPositions(computePositions(room, myUserId));
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
const onReceipt = (): void => {
if (debounceTimer !== null) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
setPositions(computePositions(room, myUserId));
debounceTimer = null;
}, 150);
};
room.on(RoomEvent.Receipt, onReceipt);
return () => {
if (debounceTimer !== null) clearTimeout(debounceTimer);
room.removeListener(RoomEvent.Receipt, onReceipt);
};
}, [room, myUserId]);