fix(perf,a11y): selectAtom for unread subscriptions, semantic headings, Perf-5 binary search

Perf-3: Replace raw roomToUnreadAtom subscription in Home, Direct, Space with
  selectAtom-derived Set<string> — components now only re-render when rooms
  gain/lose unread presence, not on every notification count update
Perf-5: RoomTimeline eventRenderer now uses binary search on precomputed
  timelineSegments instead of O(N×T) linear scan per visible message
A11y L-1: Add as=h2 semantic heading to Home, Direct, Inbox, Space page nav
  titles so screen readers announce page sections correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lotus Bot
2026-05-20 21:59:09 -04:00
parent 4e80c0a0f5
commit 18b12cfca1
4 changed files with 71 additions and 13 deletions
+24 -4
View File
@@ -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() {
<PageNavHeader>
<Box alignItems="Center" grow="Yes" gap="300">
<Box grow="Yes">
<Text size="H4" truncate>
<Text as="h2" size="H4" truncate>
Home
</Text>
</Box>
@@ -200,7 +201,26 @@ export function Home() {
const scrollRef = useRef<HTMLDivElement>(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<string>();
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,