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:
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user