Files
cinny/src/app/hooks/useRoomLatestRenderedEvent.ts
T
jared dcd8201e16 fix(wave-3): audit fixes — ACL guards, presence, moderation, theming perf
Wave-3 bug-hunt fixes (findings in LOTUS_TODO), reviewed + gate-green:
- 🔴 ACL editor [H1–H4]: block saving an empty allow-list (was a one-click
  federation brick), warn on self-ban (case-insensitive glob match of
  mx.getDomain() vs allow/deny), accept real globs (1.2.3.*, *.evil.*), and
  gate Save behind a confirm dialog.
- 🔴 [P1] room context menu no longer acts on the wrong room after a live
  reorder (key by roomId, not list index). 🔴 [P2] status writes no longer
  force presence to online over Invisible/DND (shared presenceStateFromSetting).
- 🟠 [P3] timed mutes restored on boot; [P4] custom-status auto-clear now fires
  (always-mounted StatusExpiryMonitor); [P5] timezone also PUT to the m.tz
  profile field so it's visible to others; [H6] RoomInsights single-pass
  min/max (was Math.min(...spread) stack overflow); [H7/H8] mod-log labels.
- 🟡 [P6/P7] favorites collapse+filter, [P8] charCount reset, [P9] DM preview
  refresh on decrypt; theming [T-P1] lazy decorations, [T-P2] drop the redundant
  always-on body animation, [T-P4] live useReducedMotion, [T-P5] decoration key.
- NATIVE-CINNY LAW: notification presets + Powers permissions use folds icons.

DEFERRED: [H5] invite-QR is fetched from api.qrserver.com (third-party leak);
local generation needs a bundled QR lib (not added). tsc/eslint/prettier clean,
build OK, 677 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:40:07 -04:00

74 lines
2.8 KiB
TypeScript

import {
MatrixEvent,
MatrixEventEvent,
MatrixEventHandlerMap,
Room,
RoomEvent,
RoomEventHandlerMap,
} from 'matrix-js-sdk';
import { useEffect, useState } from 'react';
import { settingsAtom } from '../state/settings';
import { useSetting } from '../state/hooks/settings';
import { MessageEvent, StateEvent } from '../../types/matrix/room';
import { isMembershipChanged, reactionOrEditEvent } from '../utils/room';
export const useRoomLatestRenderedEvent = (room: Room) => {
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
const [latestEvent, setLatestEvent] = useState<MatrixEvent>();
useEffect(() => {
const getLatestEvent = (): MatrixEvent | undefined => {
const liveEvents = room.getLiveTimeline().getEvents();
for (let i = liveEvents.length - 1; i >= 0; i -= 1) {
const evt = liveEvents[i];
if (!evt) continue;
if (reactionOrEditEvent(evt)) continue;
if (evt.getType() === StateEvent.RoomMember) {
const membershipChanged = isMembershipChanged(evt);
if (membershipChanged && hideMembershipEvents) continue;
if (!membershipChanged && hideNickAvatarEvents) continue;
return evt;
}
if (
evt.getType() === MessageEvent.RoomMessage ||
evt.getType() === MessageEvent.RoomMessageEncrypted ||
evt.getType() === MessageEvent.Sticker ||
evt.getType() === StateEvent.RoomName ||
evt.getType() === StateEvent.RoomTopic ||
evt.getType() === StateEvent.RoomAvatar
) {
return evt;
}
if (showHiddenEvents) return evt;
}
return undefined;
};
const handleTimelineEvent: RoomEventHandlerMap[RoomEvent.Timeline] = () => {
setLatestEvent(getLatestEvent());
};
// An E2EE message often arrives as an undecrypted placeholder and is decrypted
// shortly after — decryption does NOT re-fire RoomEvent.Timeline, so without this
// the DM preview stays stale ("Encrypted message") until the next timeline event.
const handleDecrypted: MatrixEventHandlerMap[MatrixEventEvent.Decrypted] = (event) => {
if (event.getRoomId() !== room.roomId) return;
setLatestEvent(getLatestEvent());
};
setLatestEvent(getLatestEvent());
room.on(RoomEvent.Timeline, handleTimelineEvent);
room.client.on(MatrixEventEvent.Decrypted, handleDecrypted);
return () => {
room.removeListener(RoomEvent.Timeline, handleTimelineEvent);
room.client.removeListener(MatrixEventEvent.Decrypted, handleDecrypted);
};
}, [room, hideMembershipEvents, hideNickAvatarEvents, showHiddenEvents]);
return latestEvent;
};