From 3f9fea1cf2fb1846876c705aff843293d53db6d1 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 7 Jul 2026 20:58:10 -0400 Subject: [PATCH] fix(receipts): refresh read-receipt avatars/names on member changes (N6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReadReceiptAvatars read avatar + display-name from room member state at render time, so they only refreshed when the parent Message re-rendered (on a receipt change). A reader changing their avatar/display-name left a stale avatar/name until an unrelated re-render. Subscribe to RoomStateEvent.Members (the only signal that fires for name, avatar AND membership changes — RoomMemberEvent has no Avatar event) scoped to the displayed readers, and force a re-render so the pill updates live. Listener is cleaned up; userIds identity is stable (memoized in the read- positions map) so no subscribe churn. Co-Authored-By: Claude Opus 4.8 --- .../ReadReceiptAvatars.tsx | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx index 2be195c63..14585d0c8 100644 --- a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx +++ b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Room } from 'matrix-js-sdk'; +import React, { useEffect, useState } from 'react'; +import { Room, RoomStateEvent, RoomStateEventHandlerMap } from 'matrix-js-sdk'; import { Icon, Icons, @@ -23,6 +23,7 @@ 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 * as css from './ReadReceiptAvatars.css'; const MAX_DISPLAY = 5; @@ -41,6 +42,28 @@ export function ReadReceiptAvatars({ 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]); if (userIds.length === 0) return null;