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:
@@ -1,5 +1,6 @@
|
|||||||
import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react';
|
import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
|
import { selectAtom } from 'jotai/utils';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
@@ -102,7 +103,7 @@ function DirectHeader() {
|
|||||||
<PageNavHeader>
|
<PageNavHeader>
|
||||||
<Box alignItems="Center" grow="Yes" gap="300">
|
<Box alignItems="Center" grow="Yes" gap="300">
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4" truncate>
|
<Text as="h2" size="H4" truncate>
|
||||||
Direct Messages
|
Direct Messages
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -174,7 +175,25 @@ export function Direct() {
|
|||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const directs = useDirectRooms();
|
const directs = useDirectRooms();
|
||||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||||
const roomToUnread = useAtomValue(roomToUnreadAtom);
|
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 navigate = useNavigate();
|
||||||
|
|
||||||
const createDirectSelected = useDirectCreateSelected();
|
const createDirectSelected = useDirectCreateSelected();
|
||||||
@@ -186,10 +205,10 @@ export function Direct() {
|
|||||||
const sortedDirects = useMemo(() => {
|
const sortedDirects = useMemo(() => {
|
||||||
const items = Array.from(directs).sort(factoryRoomIdByActivity(mx));
|
const items = Array.from(directs).sort(factoryRoomIdByActivity(mx));
|
||||||
if (closedCategories.has(DEFAULT_CATEGORY_ID)) {
|
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;
|
return items;
|
||||||
}, [mx, directs, closedCategories, roomToUnread, selectedRoomId]);
|
}, [mx, directs, closedCategories, roomsWithUnreadSet, selectedRoomId]);
|
||||||
|
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: sortedDirects.length,
|
count: sortedDirects.length,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
|
import { selectAtom } from 'jotai/utils';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { factoryRoomIdByActivity, factoryRoomIdByAtoZ } from '../../../utils/sort';
|
import { factoryRoomIdByActivity, factoryRoomIdByAtoZ } from '../../../utils/sort';
|
||||||
import {
|
import {
|
||||||
@@ -116,7 +117,7 @@ function HomeHeader() {
|
|||||||
<PageNavHeader>
|
<PageNavHeader>
|
||||||
<Box alignItems="Center" grow="Yes" gap="300">
|
<Box alignItems="Center" grow="Yes" gap="300">
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4" truncate>
|
<Text as="h2" size="H4" truncate>
|
||||||
Home
|
Home
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -200,7 +201,26 @@ export function Home() {
|
|||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const rooms = useHomeRooms();
|
const rooms = useHomeRooms();
|
||||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
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 navigate = useNavigate();
|
||||||
|
|
||||||
const selectedRoomId = useSelectedRoom();
|
const selectedRoomId = useSelectedRoom();
|
||||||
@@ -216,10 +236,10 @@ export function Home() {
|
|||||||
: factoryRoomIdByAtoZ(mx)
|
: factoryRoomIdByAtoZ(mx)
|
||||||
);
|
);
|
||||||
if (closedCategories.has(DEFAULT_CATEGORY_ID)) {
|
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;
|
return items;
|
||||||
}, [mx, rooms, closedCategories, roomToUnread, selectedRoomId]);
|
}, [mx, rooms, closedCategories, roomsWithUnreadSet, selectedRoomId]);
|
||||||
|
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: sortedRooms.length,
|
count: sortedRooms.length,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function Inbox() {
|
|||||||
<PageNavHeader>
|
<PageNavHeader>
|
||||||
<Box grow="Yes" gap="300">
|
<Box grow="Yes" gap="300">
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4" truncate>
|
<Text as="h2" size="H4" truncate>
|
||||||
Inbox
|
Inbox
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
|
import { selectAtom } from 'jotai/utils';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
@@ -267,7 +268,7 @@ function SpaceHeader() {
|
|||||||
<PageNavHeader>
|
<PageNavHeader>
|
||||||
<Box alignItems="Center" grow="Yes" gap="300">
|
<Box alignItems="Center" grow="Yes" gap="300">
|
||||||
<Box grow="Yes" alignItems="Center" gap="100">
|
<Box grow="Yes" alignItems="Center" gap="100">
|
||||||
<Text size="H4" truncate>
|
<Text as="h2" size="H4" truncate>
|
||||||
{spaceName}
|
{spaceName}
|
||||||
</Text>
|
</Text>
|
||||||
{joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />}
|
{joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />}
|
||||||
@@ -382,7 +383,25 @@ export function Space() {
|
|||||||
const spaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId);
|
const spaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const mDirects = useAtomValue(mDirectAtom);
|
const mDirects = useAtomValue(mDirectAtom);
|
||||||
const roomToUnread = useAtomValue(roomToUnreadAtom);
|
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 allRooms = useAtomValue(allRoomsAtom);
|
const allRooms = useAtomValue(allRoomsAtom);
|
||||||
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
|
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
|
||||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||||
@@ -414,10 +433,10 @@ export function Space() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const showRoomAnyway =
|
const showRoomAnyway =
|
||||||
roomToUnread.has(roomId) || roomId === selectedRoomId || callEmbed?.roomId === roomId;
|
roomsWithUnreadSet.has(roomId) || roomId === selectedRoomId || callEmbed?.roomId === roomId;
|
||||||
return !showRoomAnyway;
|
return !showRoomAnyway;
|
||||||
},
|
},
|
||||||
[space.roomId, closedCategories, roomToUnread, selectedRoomId, callEmbed]
|
[space.roomId, closedCategories, roomsWithUnreadSet, selectedRoomId, callEmbed]
|
||||||
),
|
),
|
||||||
useCallback(
|
useCallback(
|
||||||
(sId) => closedCategories.has(makeNavCategoryId(space.roomId, sId)),
|
(sId) => closedCategories.has(makeNavCategoryId(space.roomId, sId)),
|
||||||
|
|||||||
Reference in New Issue
Block a user