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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-13 00:56:22 -04:00
co-authored by Claude Opus 5
parent 31b3cf63c6
commit 4d7a510e06
2 changed files with 41 additions and 1 deletions
+31
View File
@@ -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;
};
+10 -1
View File
@@ -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<RectCords>();
// 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);