diff --git a/src/app/components/AccountDataEditor.tsx b/src/app/components/AccountDataEditor.tsx
index 2dbaf1f1f..76b73dda2 100644
--- a/src/app/components/AccountDataEditor.tsx
+++ b/src/app/components/AccountDataEditor.tsx
@@ -294,7 +294,7 @@ export function AccountDataEditor({
-
+
diff --git a/src/app/components/DeviceVerification.tsx b/src/app/components/DeviceVerification.tsx
index d2502ccf7..53875f9fc 100644
--- a/src/app/components/DeviceVerification.tsx
+++ b/src/app/components/DeviceVerification.tsx
@@ -261,7 +261,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
Device Verification
-
+
diff --git a/src/app/components/DeviceVerificationSetup.tsx b/src/app/components/DeviceVerificationSetup.tsx
index 433fa6a1d..1e13e2f97 100644
--- a/src/app/components/DeviceVerificationSetup.tsx
+++ b/src/app/components/DeviceVerificationSetup.tsx
@@ -301,7 +301,7 @@ export const DeviceVerificationSetup = forwardRef
Setup Device Verification
-
+
diff --git a/src/app/components/editor/Toolbar.tsx b/src/app/components/editor/Toolbar.tsx
index 7d701c424..b6be7b878 100644
--- a/src/app/components/editor/Toolbar.tsx
+++ b/src/app/components/editor/Toolbar.tsx
@@ -54,8 +54,8 @@ function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) {
);
}
-type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode };
-export function MarkButton({ format, icon, tooltip }: MarkButtonProps) {
+type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode; label?: string };
+export function MarkButton({ format, icon, tooltip, label }: MarkButtonProps) {
const editor = useSlate();
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
@@ -76,6 +76,7 @@ export function MarkButton({ format, icon, tooltip }: MarkButtonProps) {
variant="SurfaceVariant"
onClick={handleClick}
aria-pressed={isMarkActive(editor, format)}
+ aria-label={label}
size="400"
radii="300"
disabled={disableInline}
@@ -91,8 +92,9 @@ type BlockButtonProps = {
format: BlockType;
icon: IconSrc;
tooltip: ReactNode;
+ label?: string;
};
-export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
+export function BlockButton({ format, icon, tooltip, label }: BlockButtonProps) {
const editor = useSlate();
const handleClick = () => {
@@ -108,6 +110,7 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
variant="SurfaceVariant"
onClick={handleClick}
aria-pressed={isBlockActive(editor, format)}
+ aria-label={label}
size="400"
radii="300"
>
@@ -165,6 +168,7 @@ export function HeadingBlockButton() {
handleMenuSelect(1)}
+ aria-label="Heading 1"
size="400"
radii="300"
>
@@ -180,6 +184,7 @@ export function HeadingBlockButton() {
handleMenuSelect(2)}
+ aria-label="Heading 2"
size="400"
radii="300"
>
@@ -195,6 +200,7 @@ export function HeadingBlockButton() {
handleMenuSelect(3)}
+ aria-label="Heading 3"
size="400"
radii="300"
>
@@ -271,32 +277,44 @@ export function Toolbar() {
}
+ tooltip={}
/>
}
+ tooltip={}
/>
}
+ tooltip={}
/>
}
+ tooltip={}
/>
}
+ tooltip={}
/>
}
+ tooltip={}
/>
@@ -305,22 +323,30 @@ export function Toolbar() {
}
+ tooltip={}
/>
}
+ tooltip={}
/>
}
+ tooltip={}
/>
}
+ tooltip={}
/>
diff --git a/src/app/components/event-readers/EventReaders.tsx b/src/app/components/event-readers/EventReaders.tsx
index 14d36fb28..58b1f56ea 100644
--- a/src/app/components/event-readers/EventReaders.tsx
+++ b/src/app/components/event-readers/EventReaders.tsx
@@ -73,7 +73,7 @@ export const EventReaders = as<'div', EventReadersProps>(
Seen by
-
+
diff --git a/src/app/components/password-input/PasswordInput.tsx b/src/app/components/password-input/PasswordInput.tsx
index 4ffe0d831..3e1e278d7 100644
--- a/src/app/components/password-input/PasswordInput.tsx
+++ b/src/app/components/password-input/PasswordInput.tsx
@@ -28,6 +28,7 @@ export const PasswordInput = forwardRef(
variant={visible ? 'Warning' : variant}
size="300"
radii="300"
+ aria-label={visible ? 'Hide password' : 'Show password'}
>
Account Password
-
+
diff --git a/src/app/components/uia-stages/SSOStage.tsx b/src/app/components/uia-stages/SSOStage.tsx
index e3d6126a5..2548d1e04 100644
--- a/src/app/components/uia-stages/SSOStage.tsx
+++ b/src/app/components/uia-stages/SSOStage.tsx
@@ -56,7 +56,7 @@ export function SSOStage({
SSO Login
-
+
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index 436bbe35a..466f0caf4 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -546,6 +546,17 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const timelineRef = React.useRef(timeline);
timelineRef.current = timeline;
const eventsLength = getTimelinesEventsCount(timeline.linkedTimelines);
+
+ // Perf-5: precompute base offsets once per linkedTimelines change instead of O(N×T) scan
+ const timelineSegments = useMemo>(() => {
+ let base = 0;
+ return timeline.linkedTimelines.map((t) => {
+ const len = t.getEvents().length;
+ const seg: [number, number, EventTimeline] = [base, len, t];
+ base += len;
+ return seg;
+ });
+ }, [timeline.linkedTimelines]);
const liveTimelineLinked =
timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room);
const canPaginateBack =
@@ -1809,7 +1820,20 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
let newDivider = false;
let dayDivider = false;
const eventRenderer = (item: number) => {
- const [eventTimeline, baseIndex] = getTimelineAndBaseIndex(timeline.linkedTimelines, item);
+ // Perf-5: O(T) → O(log T) via precomputed segments
+ let eventTimeline: EventTimeline | undefined;
+ let baseIndex = 0;
+ {
+ let lo = 0;
+ let hi = timelineSegments.length - 1;
+ while (lo <= hi) {
+ const mid = (lo + hi) >>> 1;
+ const [base, len] = timelineSegments[mid];
+ if (item < base) { hi = mid - 1; }
+ else if (item >= base + len) { lo = mid + 1; }
+ else { eventTimeline = timelineSegments[mid][2]; baseIndex = base; break; }
+ }
+ }
if (!eventTimeline) return null;
const timelineSet = eventTimeline?.getTimelineSet();
const mEvent = getTimelineEvent(eventTimeline, getTimelineRelativeIndex(item, baseIndex));
diff --git a/src/app/features/settings/Settings.tsx b/src/app/features/settings/Settings.tsx
index 5e1a20f4a..46452aa34 100644
--- a/src/app/features/settings/Settings.tsx
+++ b/src/app/features/settings/Settings.tsx
@@ -141,7 +141,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
{screenSize === ScreenSize.Mobile && (
-
+
)}
diff --git a/src/app/features/settings/about/About.tsx b/src/app/features/settings/about/About.tsx
index b47acd179..96d21a147 100644
--- a/src/app/features/settings/about/About.tsx
+++ b/src/app/features/settings/about/About.tsx
@@ -25,7 +25,7 @@ export function About({ requestClose }: AboutProps) {
-
+
diff --git a/src/app/features/settings/account/Account.tsx b/src/app/features/settings/account/Account.tsx
index c4b56e475..bf3e60324 100644
--- a/src/app/features/settings/account/Account.tsx
+++ b/src/app/features/settings/account/Account.tsx
@@ -20,7 +20,7 @@ export function Account({ requestClose }: AccountProps) {
-
+
diff --git a/src/app/features/settings/account/IgnoredUserList.tsx b/src/app/features/settings/account/IgnoredUserList.tsx
index 98db9459a..6c7dccc38 100644
--- a/src/app/features/settings/account/IgnoredUserList.tsx
+++ b/src/app/features/settings/account/IgnoredUserList.tsx
@@ -72,6 +72,7 @@ function IgnoreUserInput({ userList }: { userList: string[] }) {
size="300"
radii="300"
variant="Secondary"
+ aria-label="Clear"
>
diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx
index e982a7992..895012754 100644
--- a/src/app/features/settings/account/Profile.tsx
+++ b/src/app/features/settings/account/Profile.tsx
@@ -185,7 +185,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
Remove Avatar
- setAlertRemove(false)} radii="300">
+ setAlertRemove(false)} radii="300" aria-label="Cancel">
@@ -278,6 +278,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
size="300"
radii="300"
variant="Secondary"
+ aria-label="Reset display name"
>
diff --git a/src/app/features/settings/developer-tools/DevelopTools.tsx b/src/app/features/settings/developer-tools/DevelopTools.tsx
index a3f04567b..63037ac18 100644
--- a/src/app/features/settings/developer-tools/DevelopTools.tsx
+++ b/src/app/features/settings/developer-tools/DevelopTools.tsx
@@ -51,7 +51,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
-
+
diff --git a/src/app/features/settings/devices/DeviceTile.tsx b/src/app/features/settings/devices/DeviceTile.tsx
index 71b684f56..97c284a06 100644
--- a/src/app/features/settings/devices/DeviceTile.tsx
+++ b/src/app/features/settings/devices/DeviceTile.tsx
@@ -292,6 +292,8 @@ export function DeviceTile({
outlined={deleted}
radii="300"
onClick={() => setDetails(!details)}
+ aria-label={details ? 'Collapse device details' : 'Expand device details'}
+ aria-expanded={details}
>
diff --git a/src/app/features/settings/devices/Devices.tsx b/src/app/features/settings/devices/Devices.tsx
index c957ab318..22a46f396 100644
--- a/src/app/features/settings/devices/Devices.tsx
+++ b/src/app/features/settings/devices/Devices.tsx
@@ -74,7 +74,7 @@ export function Devices({ requestClose }: DevicesProps) {
-
+
diff --git a/src/app/features/settings/devices/Verification.tsx b/src/app/features/settings/devices/Verification.tsx
index 6c7eab17b..5223df2cc 100644
--- a/src/app/features/settings/devices/Verification.tsx
+++ b/src/app/features/settings/devices/Verification.tsx
@@ -293,6 +293,7 @@ export function DeviceVerificationOptions() {
size="300"
radii="300"
onClick={handleMenu}
+ aria-label="Verification options"
>
diff --git a/src/app/features/settings/emojis-stickers/EmojisStickers.tsx b/src/app/features/settings/emojis-stickers/EmojisStickers.tsx
index 93715120c..85762b4f6 100644
--- a/src/app/features/settings/emojis-stickers/EmojisStickers.tsx
+++ b/src/app/features/settings/emojis-stickers/EmojisStickers.tsx
@@ -30,7 +30,7 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) {
-
+
diff --git a/src/app/features/settings/emojis-stickers/GlobalPacks.tsx b/src/app/features/settings/emojis-stickers/GlobalPacks.tsx
index a9288728f..6c3edc1ad 100644
--- a/src/app/features/settings/emojis-stickers/GlobalPacks.tsx
+++ b/src/app/features/settings/emojis-stickers/GlobalPacks.tsx
@@ -373,6 +373,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
variant="Critical"
onClick={() => handleUndoRemove(address)}
disabled={applyingChanges}
+ aria-label="Undo remove pack"
>
@@ -383,6 +384,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
variant="Secondary"
onClick={() => handleRemove(address)}
disabled={applyingChanges}
+ aria-label="Remove pack"
>
diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx
index 998aa90ef..45e357eac 100644
--- a/src/app/features/settings/general/General.tsx
+++ b/src/app/features/settings/general/General.tsx
@@ -1184,7 +1184,7 @@ export function General({ requestClose }: GeneralProps) {
-
+
diff --git a/src/app/features/settings/notifications/KeywordMessages.tsx b/src/app/features/settings/notifications/KeywordMessages.tsx
index 7f84d607f..5be38c399 100644
--- a/src/app/features/settings/notifications/KeywordMessages.tsx
+++ b/src/app/features/settings/notifications/KeywordMessages.tsx
@@ -80,6 +80,7 @@ function KeywordInput() {
size="300"
radii="300"
variant="Secondary"
+ aria-label="Clear keyword input"
>
@@ -118,7 +119,7 @@ function KeywordCross({ pushRule }: PushRulesProps) {
const removing = removeState.status === AsyncStatus.Loading;
return (
-
+
{removing ? : }
);
diff --git a/src/app/features/settings/notifications/Notifications.tsx b/src/app/features/settings/notifications/Notifications.tsx
index 095a9bba9..e7caae92e 100644
--- a/src/app/features/settings/notifications/Notifications.tsx
+++ b/src/app/features/settings/notifications/Notifications.tsx
@@ -23,7 +23,7 @@ export function Notifications({ requestClose }: NotificationsProps) {
-
+