Compare commits
2 Commits
888e741f94
...
fdc45db52f
| Author | SHA1 | Date | |
|---|---|---|---|
| fdc45db52f | |||
| 584da83bf0 |
@@ -294,7 +294,7 @@ export function AccountDataEditor({
|
|||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface">
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
|
|||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Device Verification</Text>
|
<Text size="H4">Device Verification</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={handleCancel}>
|
<IconButton size="300" radii="300" onClick={handleCancel} aria-label="Cancel verification">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
|||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Setup Device Verification</Text>
|
<Text size="H4">Setup Device Verification</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={onCancel}>
|
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode };
|
type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode; label?: string };
|
||||||
export function MarkButton({ format, icon, tooltip }: MarkButtonProps) {
|
export function MarkButton({ format, icon, tooltip, label }: MarkButtonProps) {
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
|
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
|
||||||
|
|
||||||
@@ -76,6 +76,7 @@ export function MarkButton({ format, icon, tooltip }: MarkButtonProps) {
|
|||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-pressed={isMarkActive(editor, format)}
|
aria-pressed={isMarkActive(editor, format)}
|
||||||
|
aria-label={label}
|
||||||
size="400"
|
size="400"
|
||||||
radii="300"
|
radii="300"
|
||||||
disabled={disableInline}
|
disabled={disableInline}
|
||||||
@@ -91,8 +92,9 @@ type BlockButtonProps = {
|
|||||||
format: BlockType;
|
format: BlockType;
|
||||||
icon: IconSrc;
|
icon: IconSrc;
|
||||||
tooltip: ReactNode;
|
tooltip: ReactNode;
|
||||||
|
label?: string;
|
||||||
};
|
};
|
||||||
export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
|
export function BlockButton({ format, icon, tooltip, label }: BlockButtonProps) {
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@@ -108,6 +110,7 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
|
|||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-pressed={isBlockActive(editor, format)}
|
aria-pressed={isBlockActive(editor, format)}
|
||||||
|
aria-label={label}
|
||||||
size="400"
|
size="400"
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
@@ -165,6 +168,7 @@ export function HeadingBlockButton() {
|
|||||||
<IconButton
|
<IconButton
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
onClick={() => handleMenuSelect(1)}
|
onClick={() => handleMenuSelect(1)}
|
||||||
|
aria-label="Heading 1"
|
||||||
size="400"
|
size="400"
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
@@ -180,6 +184,7 @@ export function HeadingBlockButton() {
|
|||||||
<IconButton
|
<IconButton
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
onClick={() => handleMenuSelect(2)}
|
onClick={() => handleMenuSelect(2)}
|
||||||
|
aria-label="Heading 2"
|
||||||
size="400"
|
size="400"
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
@@ -195,6 +200,7 @@ export function HeadingBlockButton() {
|
|||||||
<IconButton
|
<IconButton
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
onClick={() => handleMenuSelect(3)}
|
onClick={() => handleMenuSelect(3)}
|
||||||
|
aria-label="Heading 3"
|
||||||
size="400"
|
size="400"
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
@@ -271,32 +277,44 @@ export function Toolbar() {
|
|||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Bold}
|
format={MarkType.Bold}
|
||||||
icon={Icons.Bold}
|
icon={Icons.Bold}
|
||||||
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} />}
|
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`}
|
||||||
|
label="Bold"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Italic}
|
format={MarkType.Italic}
|
||||||
icon={Icons.Italic}
|
icon={Icons.Italic}
|
||||||
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} />}
|
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`}
|
||||||
|
label="Italic"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Underline}
|
format={MarkType.Underline}
|
||||||
icon={Icons.Underline}
|
icon={Icons.Underline}
|
||||||
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`} />}
|
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`}
|
||||||
|
label="Underline"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.StrikeThrough}
|
format={MarkType.StrikeThrough}
|
||||||
icon={Icons.Strike}
|
icon={Icons.Strike}
|
||||||
tooltip={<BtnTooltip text="Strike Through" shortCode={`${modKey} + S`} />}
|
tooltip={<BtnTooltip text="Strike Through" shortCode={`${modKey} + S`}
|
||||||
|
label="Strikethrough"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Code}
|
format={MarkType.Code}
|
||||||
icon={Icons.Code}
|
icon={Icons.Code}
|
||||||
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} />}
|
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`}
|
||||||
|
label="Inline code"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Spoiler}
|
format={MarkType.Spoiler}
|
||||||
icon={Icons.EyeBlind}
|
icon={Icons.EyeBlind}
|
||||||
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} />}
|
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`}
|
||||||
|
label="Spoiler"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
|
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
|
||||||
@@ -305,22 +323,30 @@ export function Toolbar() {
|
|||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.BlockQuote}
|
format={BlockType.BlockQuote}
|
||||||
icon={Icons.BlockQuote}
|
icon={Icons.BlockQuote}
|
||||||
tooltip={<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`} />}
|
tooltip={<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`}
|
||||||
|
label="Block quote"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.CodeBlock}
|
format={BlockType.CodeBlock}
|
||||||
icon={Icons.BlockCode}
|
icon={Icons.BlockCode}
|
||||||
tooltip={<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`} />}
|
tooltip={<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`}
|
||||||
|
label="Code block"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.OrderedList}
|
format={BlockType.OrderedList}
|
||||||
icon={Icons.OrderList}
|
icon={Icons.OrderList}
|
||||||
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} />}
|
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`}
|
||||||
|
label="Ordered list"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.UnorderedList}
|
format={BlockType.UnorderedList}
|
||||||
icon={Icons.UnorderList}
|
icon={Icons.UnorderList}
|
||||||
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`} />}
|
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`}
|
||||||
|
label="Unordered list"
|
||||||
|
/>}
|
||||||
/>
|
/>
|
||||||
<HeadingBlockButton />
|
<HeadingBlockButton />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const EventReaders = as<'div', EventReadersProps>(
|
|||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H3" style={lotusTerminal ? { color: '#00D4FF', textShadow: '0 0 6px rgba(0,212,255,0.45)', letterSpacing: '0.05em' } : undefined}>Seen by</Text>
|
<Text size="H3" style={lotusTerminal ? { color: '#00D4FF', textShadow: '0 0 6px rgba(0,212,255,0.45)', letterSpacing: '0.05em' } : undefined}>Seen by</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={requestClose}>
|
<IconButton size="300" onClick={requestClose} aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
|||||||
variant={visible ? 'Warning' : variant}
|
variant={visible ? 'Warning' : variant}
|
||||||
size="300"
|
size="300"
|
||||||
radii="300"
|
radii="300"
|
||||||
|
aria-label={visible ? 'Hide password' : 'Show password'}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
style={{ opacity: config.opacity.P300 }}
|
style={{ opacity: config.opacity.P300 }}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function PasswordStage({
|
|||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Account Password</Text>
|
<Text size="H4">Account Password</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300">
|
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function SSOStage({
|
|||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">SSO Login</Text>
|
<Text size="H4">SSO Login</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300">
|
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -546,6 +546,17 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
const timelineRef = React.useRef(timeline);
|
const timelineRef = React.useRef(timeline);
|
||||||
timelineRef.current = timeline;
|
timelineRef.current = timeline;
|
||||||
const eventsLength = getTimelinesEventsCount(timeline.linkedTimelines);
|
const eventsLength = getTimelinesEventsCount(timeline.linkedTimelines);
|
||||||
|
|
||||||
|
// Perf-5: precompute base offsets once per linkedTimelines change instead of O(N×T) scan
|
||||||
|
const timelineSegments = useMemo<Array<[number, number, EventTimeline]>>(() => {
|
||||||
|
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 =
|
const liveTimelineLinked =
|
||||||
timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room);
|
timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room);
|
||||||
const canPaginateBack =
|
const canPaginateBack =
|
||||||
@@ -1809,7 +1820,20 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
let newDivider = false;
|
let newDivider = false;
|
||||||
let dayDivider = false;
|
let dayDivider = false;
|
||||||
const eventRenderer = (item: number) => {
|
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;
|
if (!eventTimeline) return null;
|
||||||
const timelineSet = eventTimeline?.getTimelineSet();
|
const timelineSet = eventTimeline?.getTimelineSet();
|
||||||
const mEvent = getTimelineEvent(eventTimeline, getTimelineRelativeIndex(item, baseIndex));
|
const mEvent = getTimelineEvent(eventTimeline, getTimelineRelativeIndex(item, baseIndex));
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
{screenSize === ScreenSize.Mobile && (
|
{screenSize === ScreenSize.Mobile && (
|
||||||
<IconButton onClick={requestClose} variant="Background">
|
<IconButton onClick={requestClose} variant="Background" aria-label="Close settings">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function About({ requestClose }: AboutProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface">
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function Account({ requestClose }: AccountProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface">
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ function IgnoreUserInput({ userList }: { userList: string[] }) {
|
|||||||
size="300"
|
size="300"
|
||||||
radii="300"
|
radii="300"
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
|
aria-label="Clear"
|
||||||
>
|
>
|
||||||
<Icon src={Icons.Cross} size="100" />
|
<Icon src={Icons.Cross} size="100" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
|
|||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Remove Avatar</Text>
|
<Text size="H4">Remove Avatar</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300">
|
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -278,6 +278,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
|
|||||||
size="300"
|
size="300"
|
||||||
radii="300"
|
radii="300"
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
|
aria-label="Reset display name"
|
||||||
>
|
>
|
||||||
<Icon src={Icons.Cross} size="100" />
|
<Icon src={Icons.Cross} size="100" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface">
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -292,6 +292,8 @@ export function DeviceTile({
|
|||||||
outlined={deleted}
|
outlined={deleted}
|
||||||
radii="300"
|
radii="300"
|
||||||
onClick={() => setDetails(!details)}
|
onClick={() => setDetails(!details)}
|
||||||
|
aria-label={details ? 'Collapse device details' : 'Expand device details'}
|
||||||
|
aria-expanded={details}
|
||||||
>
|
>
|
||||||
<Icon size="50" src={details ? Icons.ChevronBottom : Icons.ChevronRight} />
|
<Icon size="50" src={details ? Icons.ChevronBottom : Icons.ChevronRight} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function Devices({ requestClose }: DevicesProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface">
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ export function DeviceVerificationOptions() {
|
|||||||
size="300"
|
size="300"
|
||||||
radii="300"
|
radii="300"
|
||||||
onClick={handleMenu}
|
onClick={handleMenu}
|
||||||
|
aria-label="Verification options"
|
||||||
>
|
>
|
||||||
<Icon size="100" src={Icons.VerticalDots} />
|
<Icon size="100" src={Icons.VerticalDots} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface">
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
|
|||||||
variant="Critical"
|
variant="Critical"
|
||||||
onClick={() => handleUndoRemove(address)}
|
onClick={() => handleUndoRemove(address)}
|
||||||
disabled={applyingChanges}
|
disabled={applyingChanges}
|
||||||
|
aria-label="Undo remove pack"
|
||||||
>
|
>
|
||||||
<Icon src={Icons.Plus} size="100" />
|
<Icon src={Icons.Plus} size="100" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -383,6 +384,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
|
|||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
onClick={() => handleRemove(address)}
|
onClick={() => handleRemove(address)}
|
||||||
disabled={applyingChanges}
|
disabled={applyingChanges}
|
||||||
|
aria-label="Remove pack"
|
||||||
>
|
>
|
||||||
<Icon src={Icons.Cross} size="100" />
|
<Icon src={Icons.Cross} size="100" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -1184,7 +1184,7 @@ export function General({ requestClose }: GeneralProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface">
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ function KeywordInput() {
|
|||||||
size="300"
|
size="300"
|
||||||
radii="300"
|
radii="300"
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
|
aria-label="Clear keyword input"
|
||||||
>
|
>
|
||||||
<Icon src={Icons.Cross} size="100" />
|
<Icon src={Icons.Cross} size="100" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -118,7 +119,7 @@ function KeywordCross({ pushRule }: PushRulesProps) {
|
|||||||
|
|
||||||
const removing = removeState.status === AsyncStatus.Loading;
|
const removing = removeState.status === AsyncStatus.Loading;
|
||||||
return (
|
return (
|
||||||
<IconButton onClick={remove} size="300" radii="Pill" variant="Secondary" disabled={removing}>
|
<IconButton onClick={remove} size="300" radii="Pill" variant="Secondary" disabled={removing} aria-label="Remove keyword">
|
||||||
{removing ? <Spinner size="100" /> : <Icon src={Icons.Cross} size="100" />}
|
{removing ? <Spinner size="100" /> : <Icon src={Icons.Cross} size="100" />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function Notifications({ requestClose }: NotificationsProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface">
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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