From 4d7a510e06ad2cadb9decc09fd20aaab2dd6196e Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 13 Sep 2026 00:56:22 -0400 Subject: [PATCH] fix(sidebar): favourite / low-priority changes move the room immediately Home's categorisation memo keyed only on the room list, and nothing observed m.tag changes. The SDK emits RoomEvent.Tags on the room and re-emits it on the client (room.js addTags, sync.js reEmit); a small client-level hook bumps a version that the memo depends on. Fixes #20 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/hooks/useRoomTagsVersion.ts | 31 +++++++++++++++++++++++++++++ src/app/pages/client/home/Home.tsx | 11 +++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/app/hooks/useRoomTagsVersion.ts diff --git a/src/app/hooks/useRoomTagsVersion.ts b/src/app/hooks/useRoomTagsVersion.ts new file mode 100644 index 000000000..fb14db575 --- /dev/null +++ b/src/app/hooks/useRoomTagsVersion.ts @@ -0,0 +1,31 @@ +import { useEffect } from 'react'; +import { MatrixClient, RoomEvent } from 'matrix-js-sdk'; +import { useForceUpdate } from './useForceUpdate'; + +/** + * Bumps a counter whenever any room's `m.tag` account data changes + * (favourite/low-priority add or remove via `mx.setRoomTag`/`deleteRoomTag`). + * + * `RoomEvent.Tags` is emitted on the individual `Room` by `Room.addTags()` + * (matrix-js-sdk `lib/models/room.js`) and re-emitted onto the `MatrixClient` + * itself by the sync loop (`lib/sync.js`, the `client.reEmitter.reEmit(room, [... + * RoomEvent.Tags ...])` call), so a single client-level listener here sees the + * change for every room without needing to attach/detach a per-room listener. + * + * Consumers should add the returned counter to the deps of any memo that + * derives favourite/low-priority categorisation from `room.tags`, so toggling + * a tag moves the room immediately instead of waiting for an unrelated + * re-render to pick up the mutated (but stale-looking) `Room` object. + */ +export const useRoomTagsVersion = (mx: MatrixClient): number => { + const [version, bumpVersion] = useForceUpdate(); + + useEffect(() => { + mx.on(RoomEvent.Tags, bumpVersion); + return () => { + mx.removeListener(RoomEvent.Tags, bumpVersion); + }; + }, [mx, bumpVersion]); + + return version; +}; diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx index ebd742109..068aebc02 100644 --- a/src/app/pages/client/home/Home.tsx +++ b/src/app/pages/client/home/Home.tsx @@ -79,6 +79,7 @@ import { UseStateProvider } from '../../../components/UseStateProvider'; import { JoinAddressPrompt } from '../../../components/join-address-prompt'; import { _RoomSearchParams } from '../../paths'; import { getLocalRoomNamesContent } from '../../../hooks/useRoomMeta'; +import { useRoomTagsVersion } from '../../../hooks/useRoomTagsVersion'; type HomeMenuProps = { requestClose: () => void; @@ -253,6 +254,11 @@ export function Home() { const [homeRoomSort, setHomeRoomSort] = useSetting(settingsAtom, 'homeRoomSort'); const roomToUnread = useAtomValue(roomToUnreadAtom); const [sortMenuAnchor, setSortMenuAnchor] = useState(); + // Bumped whenever any room's `m.tag` account data changes (favourite/low-priority + // toggle via RoomNavItemMenu). Nothing else re-runs this memo on a tag change + // (see useRoomTagsVersion.ts), so without this dep a room only moves section + // once some unrelated event (e.g. an unread-count change) forces a re-render. + const roomTagsVersion = useRoomTagsVersion(mx); const { favoriteRooms, lowPriorityRooms, otherRooms } = useMemo(() => { const favs: string[] = []; @@ -269,7 +275,10 @@ export function Home() { } }); return { favoriteRooms: favs, lowPriorityRooms: low, otherRooms: others }; - }, [mx, rooms]); + // roomTagsVersion is a trigger-only counter, not read in the body; it forces + // this memo to re-run whenever any room's tags change. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mx, rooms, roomTagsVersion]); const sortedFavoriteRooms = useMemo(() => { const isClosed = closedCategories.has(FAVORITES_CATEGORY_ID);