diff --git a/src/app/pages/client/direct/Direct.tsx b/src/app/pages/client/direct/Direct.tsx index e125df0f0..74e34d2b8 100644 --- a/src/app/pages/client/direct/Direct.tsx +++ b/src/app/pages/client/direct/Direct.tsx @@ -1,5 +1,6 @@ import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react'; import { useAtom, useAtomValue } from 'jotai'; +import { selectAtom } from 'jotai/utils'; import { Avatar, Box, @@ -102,7 +103,7 @@ function DirectHeader() { - + Direct Messages @@ -174,7 +175,25 @@ export function Direct() { const scrollRef = useRef(null); const directs = useDirectRooms(); const notificationPreferences = useRoomsNotificationPreferencesContext(); - const roomToUnread = useAtomValue(roomToUnreadAtom); + const roomsWithUnreadSet = useAtomValue( + useMemo( + () => + selectAtom( + roomToUnreadAtom, + (rtu) => { + const s = new Set(); + for (const id of rtu.keys()) s.add(id); + return s; + }, + (a, b) => { + if (a.size !== b.size) return false; + for (const id of a) if (!b.has(id)) return false; + return true; + } + ), + [] + ) + ); const navigate = useNavigate(); const createDirectSelected = useDirectCreateSelected(); @@ -186,10 +205,10 @@ export function Direct() { const sortedDirects = useMemo(() => { const items = Array.from(directs).sort(factoryRoomIdByActivity(mx)); if (closedCategories.has(DEFAULT_CATEGORY_ID)) { - return items.filter((rId) => roomToUnread.has(rId) || rId === selectedRoomId); + return items.filter((rId) => roomsWithUnreadSet.has(rId) || rId === selectedRoomId); } return items; - }, [mx, directs, closedCategories, roomToUnread, selectedRoomId]); + }, [mx, directs, closedCategories, roomsWithUnreadSet, selectedRoomId]); const virtualizer = useVirtualizer({ count: sortedDirects.length, diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx index c216742c4..a2f1142d2 100644 --- a/src/app/pages/client/home/Home.tsx +++ b/src/app/pages/client/home/Home.tsx @@ -17,6 +17,7 @@ import { } from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useAtom, useAtomValue } from 'jotai'; +import { selectAtom } from 'jotai/utils'; import FocusTrap from 'focus-trap-react'; import { factoryRoomIdByActivity, factoryRoomIdByAtoZ } from '../../../utils/sort'; import { @@ -116,7 +117,7 @@ function HomeHeader() { - + Home @@ -200,7 +201,26 @@ export function Home() { const scrollRef = useRef(null); const rooms = useHomeRooms(); const notificationPreferences = useRoomsNotificationPreferencesContext(); - const roomToUnread = useAtomValue(roomToUnreadAtom); + // Perf-3: only re-render when the set of rooms WITH unread changes, not on count updates + const roomsWithUnreadSet = useAtomValue( + useMemo( + () => + selectAtom( + roomToUnreadAtom, + (rtu) => { + const s = new Set(); + for (const id of rtu.keys()) s.add(id); + return s; + }, + (a, b) => { + if (a.size !== b.size) return false; + for (const id of a) if (!b.has(id)) return false; + return true; + } + ), + [] + ) + ); const navigate = useNavigate(); const selectedRoomId = useSelectedRoom(); @@ -216,10 +236,10 @@ export function Home() { : factoryRoomIdByAtoZ(mx) ); if (closedCategories.has(DEFAULT_CATEGORY_ID)) { - return items.filter((rId) => roomToUnread.has(rId) || rId === selectedRoomId); + return items.filter((rId) => roomsWithUnreadSet.has(rId) || rId === selectedRoomId); } return items; - }, [mx, rooms, closedCategories, roomToUnread, selectedRoomId]); + }, [mx, rooms, closedCategories, roomsWithUnreadSet, selectedRoomId]); const virtualizer = useVirtualizer({ count: sortedRooms.length, diff --git a/src/app/pages/client/inbox/Inbox.tsx b/src/app/pages/client/inbox/Inbox.tsx index 67d6021a3..144e69050 100644 --- a/src/app/pages/client/inbox/Inbox.tsx +++ b/src/app/pages/client/inbox/Inbox.tsx @@ -52,7 +52,7 @@ export function Inbox() { - + Inbox diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 1f18afe4e..f4207eb3e 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -7,6 +7,7 @@ import React, { useState, } from 'react'; import { useAtom, useAtomValue } from 'jotai'; +import { selectAtom } from 'jotai/utils'; import { Avatar, Box, @@ -267,7 +268,7 @@ function SpaceHeader() { - + {spaceName} {joinRules?.join_rule !== JoinRule.Public && } @@ -382,7 +383,25 @@ export function Space() { const spaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId); const scrollRef = useRef(null); const mDirects = useAtomValue(mDirectAtom); - const roomToUnread = useAtomValue(roomToUnreadAtom); + const roomsWithUnreadSet = useAtomValue( + useMemo( + () => + selectAtom( + roomToUnreadAtom, + (rtu) => { + const s = new Set(); + for (const id of rtu.keys()) s.add(id); + return s; + }, + (a, b) => { + if (a.size !== b.size) return false; + for (const id of a) if (!b.has(id)) return false; + return true; + } + ), + [] + ) + ); const allRooms = useAtomValue(allRoomsAtom); const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const notificationPreferences = useRoomsNotificationPreferencesContext(); @@ -414,10 +433,10 @@ export function Space() { return false; } const showRoomAnyway = - roomToUnread.has(roomId) || roomId === selectedRoomId || callEmbed?.roomId === roomId; + roomsWithUnreadSet.has(roomId) || roomId === selectedRoomId || callEmbed?.roomId === roomId; return !showRoomAnyway; }, - [space.roomId, closedCategories, roomToUnread, selectedRoomId, callEmbed] + [space.roomId, closedCategories, roomsWithUnreadSet, selectedRoomId, callEmbed] ), useCallback( (sId) => closedCategories.has(makeNavCategoryId(space.roomId, sId)),