perf(receipts): shared member-change store instead of per-row listeners (PERF-3)
Every ReadReceiptAvatars row and every useMemberAvatar registered its own global RoomStateEvent.Members listener — ~6 per receipt row — each firing on any membership / display-name / avatar change in ANY room. Add a module-level MemberChangeStore (mirroring the PERF-1 presence store) that registers exactly ONE global Members listener and fans out to subscribers keyed by roomId|userId. Two hooks: useRoomMemberChange (single) and useRoomMembersChange (multi, one effect). useMemberAvatar and ReadReceiptAvatars use them; behavior (re-render triggers) is byte-for-byte equivalent. Unsubscribe is idempotent via a set-identity guard; the multi-hook key is order-independent. Unit-tested (key-scoped fan-out, single shared listener, idempotent unsubscribe). Reviewed by two passes (lifecycle/closure + behavioral equivalence) — clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Room, RoomStateEvent, RoomStateEventHandlerMap } from 'matrix-js-sdk';
|
||||
import React, { useState } from 'react';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import {
|
||||
Icon,
|
||||
Icons,
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
config,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { getMemberName } from '../../utils/room';
|
||||
@@ -21,8 +20,8 @@ import { StackedAvatar } from '../stacked-avatar';
|
||||
import { EventReaders } from '../event-readers';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { useModalStyle } from '../../hooks/useModalStyle';
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { useMemberAvatar } from '../../hooks/useMemberAvatar';
|
||||
import { useRoomMembersChange } from '../../hooks/useRoomMemberChange';
|
||||
import * as css from './ReadReceiptAvatars.css';
|
||||
|
||||
const MAX_DISPLAY = 5;
|
||||
@@ -50,32 +49,15 @@ export function ReadReceiptAvatars({
|
||||
eventId: string;
|
||||
userIds: string[];
|
||||
}) {
|
||||
const mx = useMatrixClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||
const modalStyle = useModalStyle(360);
|
||||
const [, forceUpdate] = useForceUpdate();
|
||||
|
||||
// The avatars + names below are read from room member state at render time, so
|
||||
// they only refresh when this component re-renders (which the timeline does on a
|
||||
// receipt change). Re-render on a member-state change of any displayed reader so
|
||||
// an avatar/display-name update shows live. RoomStateEvent.Members fires for name,
|
||||
// avatar AND membership changes (RoomMemberEvent has no Avatar signal).
|
||||
useEffect(() => {
|
||||
const handleMembers: RoomStateEventHandlerMap[RoomStateEvent.Members] = (
|
||||
event,
|
||||
_state,
|
||||
member,
|
||||
) => {
|
||||
if (event.getRoomId() === room.roomId && userIds.includes(member.userId)) {
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
mx.on(RoomStateEvent.Members, handleMembers);
|
||||
return () => {
|
||||
mx.removeListener(RoomStateEvent.Members, handleMembers);
|
||||
};
|
||||
}, [mx, room.roomId, userIds, forceUpdate]);
|
||||
// The tooltip names below are read from room member state at render time.
|
||||
// Re-render on a member-state change of any displayed reader so a display-name
|
||||
// update shows live (each avatar handles its own via useMemberAvatar). Uses the
|
||||
// shared member-change store — one global listener for the whole app (PERF-3).
|
||||
useRoomMembersChange(room.roomId, userIds);
|
||||
|
||||
if (userIds.length === 0) return null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user