Security, performance, bug fixes, and TDS improvements

Security:
- HIGH-1: Validate hex color format before CSS interpolation in sanitize.ts
- HIGH-5: Add sandbox attribute to OpenStreetMap iframe
- MED-1: Fix permissive URL scheme regex in LINKIFY_OPTS
- MED-3/HIGH-4: Add .js.map blocking + CSP header to nginx config
- LOW-2: Validate OIDC authUrl scheme before window.open
- Accessibility: Remove maximum-scale=1.0 from viewport meta (WCAG 1.4.4)

Performance:
- O(1) Map index in computePositions (was O(M×T) findIndex per member)
- Add RoomMemberEvent.Membership subscription so positions update on join/leave
- Fix uncleaned 2000ms setTimeout in RoomTimeline useLayoutEffect

Bug fixes:
- BUG-5: Add QUEUED/CANCELLED cases to DeliveryStatus component
- BUG-6: Guard DeliveryStatus against state events via isState() check
- BUG-10: Clamp PiP position on window resize
- BUG-14: Separate runLotusBootSequence into dedicated useEffect([lotusTerminal])
- Fix aria-live on typing indicator (WCAG 4.1.3)
- Add aria-label + aria-multiline to message editor

TDS (Lotus Terminal Design System):
- Add reaction chip styles (dark + light mode)
- Add GIF picker CSS via globalStyle instead of runtime injection
- Add URL preview styles (dark + light mode)
- Add complete GIF picker light-mode TDS block (was missing)
This commit is contained in:
Lotus Bot
2026-05-19 16:26:25 -04:00
parent 3196d6ac3e
commit 8666daaf9d
11 changed files with 77 additions and 12 deletions
+15 -4
View File
@@ -1,12 +1,16 @@
import { Room, RoomEvent, MatrixEvent } from 'matrix-js-sdk';
import { Room, RoomEvent, RoomMemberEvent, 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);
function nearestRenderableId(
liveEvents: MatrixEvent[],
eventIndex: Map<string, number>,
evtId: string
): string | null {
const idx = eventIndex.get(evtId) ?? -1;
if (idx === -1) return null;
for (let i = idx; i >= 0; i--) {
const e = liveEvents[i];
@@ -18,11 +22,15 @@ function nearestRenderableId(liveEvents: MatrixEvent[], evtId: string): string |
function computePositions(room: Room, myUserId: string): Map<string, string[]> {
const map = new Map<string, string[]>();
const liveEvents = room.getLiveTimeline().getEvents();
// Build O(1) index once instead of O(T) findIndex per member
const eventIndex = new Map<string, number>(
liveEvents.map((e, i) => [e.getId() ?? '', i])
);
for (const member of room.getJoinedMembers()) {
if (member.userId === myUserId) continue;
const evtId = room.getEventReadUpTo(member.userId);
if (!evtId) continue;
const targetId = nearestRenderableId(liveEvents, evtId);
const targetId = nearestRenderableId(liveEvents, eventIndex, evtId);
if (!targetId) continue;
const arr = map.get(targetId);
if (arr) arr.push(member.userId);
@@ -46,10 +54,13 @@ export function useRoomReadPositions(room: Room): Map<string, string[]> {
debounceTimer = null;
}, 150);
};
const onMembership = (): void => setPositions(computePositions(room, myUserId));
room.on(RoomEvent.Receipt, onReceipt);
room.on(RoomMemberEvent.Membership, onMembership);
return () => {
if (debounceTimer !== null) clearTimeout(debounceTimer);
room.removeListener(RoomEvent.Receipt, onReceipt);
room.removeListener(RoomMemberEvent.Membership, onMembership);
};
}, [room, myUserId]);