fix(bug,perf): poll first-vote race, stale timeline ref, lazy GifPicker/EmojiBoard, focusItem timer leak, RoomNavItem memo
BUG-18: clearTimeout cleanup in focusItem useLayoutEffect prevents leaked timers BUG-24: Room timeline listener catches first poll vote before Relations object exists BUG-25: Use timelineRef.current in handleOpenEvent to prevent stale index on rapid navigation Perf-6: React.lazy + Suspense for GifPicker and EmojiBoard (initial bundle -114 kB) Perf-7: React.memo on RoomNavItem to prevent re-renders on unrelated state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Box, Text } from 'folds';
|
import { Box, Text } from 'folds';
|
||||||
import { RelationsEvent } from 'matrix-js-sdk/lib/models/relations';
|
import { RelationsEvent } from 'matrix-js-sdk/lib/models/relations';
|
||||||
|
import { RoomEvent } from 'matrix-js-sdk';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
|
||||||
type PollTextValue = Array<{ body: string }> | string;
|
type PollTextValue = Array<{ body: string }> | string;
|
||||||
@@ -142,6 +143,21 @@ export function PollContent({
|
|||||||
unstableRels?.on(RelationsEvent.Add, refresh);
|
unstableRels?.on(RelationsEvent.Add, refresh);
|
||||||
unstableRels?.on(RelationsEvent.Remove, refresh);
|
unstableRels?.on(RelationsEvent.Remove, refresh);
|
||||||
unstableRels?.on(RelationsEvent.Redaction, refresh);
|
unstableRels?.on(RelationsEvent.Redaction, refresh);
|
||||||
|
// Also listen at room level: if no votes exist yet, the Relations object is null
|
||||||
|
// and the listeners above are no-ops. The room timeline event catches the first vote.
|
||||||
|
const onTimeline = (ev: any) => {
|
||||||
|
const type = ev.getType?.();
|
||||||
|
const relatesTo = ev.getContent?.()?.['m.relates_to'];
|
||||||
|
if (
|
||||||
|
(type === 'm.poll.response' || type === 'org.matrix.msc3381.poll.response') &&
|
||||||
|
relatesTo?.event_id === eventId
|
||||||
|
) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const room2 = mx.getRoom(roomId);
|
||||||
|
room2?.on(RoomEvent.Timeline, onTimeline);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stableRels?.off(RelationsEvent.Add, refresh);
|
stableRels?.off(RelationsEvent.Add, refresh);
|
||||||
stableRels?.off(RelationsEvent.Remove, refresh);
|
stableRels?.off(RelationsEvent.Remove, refresh);
|
||||||
@@ -149,6 +165,7 @@ export function PollContent({
|
|||||||
unstableRels?.off(RelationsEvent.Add, refresh);
|
unstableRels?.off(RelationsEvent.Add, refresh);
|
||||||
unstableRels?.off(RelationsEvent.Remove, refresh);
|
unstableRels?.off(RelationsEvent.Remove, refresh);
|
||||||
unstableRels?.off(RelationsEvent.Redaction, refresh);
|
unstableRels?.off(RelationsEvent.Redaction, refresh);
|
||||||
|
room2?.off(RoomEvent.Timeline, onTimeline);
|
||||||
};
|
};
|
||||||
}, [mx, roomId, eventId, refresh]);
|
}, [mx, roomId, eventId, refresh]);
|
||||||
|
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ type RoomNavItemProps = {
|
|||||||
showAvatar?: boolean;
|
showAvatar?: boolean;
|
||||||
direct?: boolean;
|
direct?: boolean;
|
||||||
};
|
};
|
||||||
export function RoomNavItem({
|
function RoomNavItem_({
|
||||||
room,
|
room,
|
||||||
selected,
|
selected,
|
||||||
showAvatar,
|
showAvatar,
|
||||||
@@ -440,3 +440,4 @@ export function RoomNavItem({
|
|||||||
</NavItem>
|
</NavItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export const RoomNavItem = React.memo(RoomNavItem_);
|
||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
} from 'folds';
|
} from 'folds';
|
||||||
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { GifPicker } from '../../components/GifPicker';
|
const GifPicker = React.lazy(() => import('../../components/GifPicker').then((m) => ({ default: m.GifPicker })));
|
||||||
import { useClientConfig } from '../../hooks/useClientConfig';
|
import { useClientConfig } from '../../hooks/useClientConfig';
|
||||||
import {
|
import {
|
||||||
CustomEditor,
|
CustomEditor,
|
||||||
@@ -56,7 +56,8 @@ import {
|
|||||||
trimCommand,
|
trimCommand,
|
||||||
getMentions,
|
getMentions,
|
||||||
} from '../../components/editor';
|
} from '../../components/editor';
|
||||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
import { EmojiBoardTab } from '../../components/emoji-board/types';
|
||||||
|
const EmojiBoard = React.lazy(() => import('../../components/emoji-board').then((m) => ({ default: m.EmojiBoard })));
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import {
|
import {
|
||||||
TUploadContent,
|
TUploadContent,
|
||||||
@@ -690,7 +691,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
||||||
}
|
}
|
||||||
content={
|
content={
|
||||||
<EmojiBoard
|
<React.Suspense fallback={null}><EmojiBoard
|
||||||
tab={emojiBoardTab}
|
tab={emojiBoardTab}
|
||||||
onTabChange={setEmojiBoardTab}
|
onTabChange={setEmojiBoardTab}
|
||||||
imagePackRooms={imagePackRooms}
|
imagePackRooms={imagePackRooms}
|
||||||
@@ -707,7 +708,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
return t;
|
return t;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/></React.Suspense>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!hideStickerBtn && (
|
{!hideStickerBtn && (
|
||||||
@@ -758,11 +759,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
content={
|
content={
|
||||||
<GifPicker
|
<React.Suspense fallback={null}><GifPicker
|
||||||
apiKey={gifApiKey}
|
apiKey={gifApiKey}
|
||||||
onSelect={handleGifSelect}
|
onSelect={handleGifSelect}
|
||||||
requestClose={() => setGifOpen(false)}
|
requestClose={() => setGifOpen(false)}
|
||||||
/>
|
/></React.Suspense>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -543,6 +543,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
const [timeline, setTimeline] = useState<Timeline>(() =>
|
const [timeline, setTimeline] = useState<Timeline>(() =>
|
||||||
eventId ? getEmptyTimeline() : getInitialTimeline(room)
|
eventId ? getEmptyTimeline() : getInitialTimeline(room)
|
||||||
);
|
);
|
||||||
|
const timelineRef = React.useRef(timeline);
|
||||||
|
timelineRef.current = timeline;
|
||||||
const eventsLength = getTimelinesEventsCount(timeline.linkedTimelines);
|
const eventsLength = getTimelinesEventsCount(timeline.linkedTimelines);
|
||||||
const liveTimelineLinked =
|
const liveTimelineLinked =
|
||||||
timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room);
|
timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room);
|
||||||
@@ -659,7 +661,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
) => {
|
) => {
|
||||||
const evtTimeline = getEventTimeline(room, evtId);
|
const evtTimeline = getEventTimeline(room, evtId);
|
||||||
const absoluteIndex =
|
const absoluteIndex =
|
||||||
evtTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, evtTimeline, evtId);
|
evtTimeline && getEventIdAbsoluteIndex(timelineRef.current.linkedTimelines, evtTimeline, evtId);
|
||||||
|
|
||||||
if (typeof absoluteIndex === 'number') {
|
if (typeof absoluteIndex === 'number') {
|
||||||
const scrolled = scrollToItem(absoluteIndex, {
|
const scrolled = scrollToItem(absoluteIndex, {
|
||||||
@@ -678,7 +680,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
loadEventTimeline(evtId);
|
loadEventTimeline(evtId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[room, timeline, scrollToItem, loadEventTimeline]
|
[room, scrollToItem, loadEventTimeline]
|
||||||
);
|
);
|
||||||
|
|
||||||
useLiveTimelineRefresh(
|
useLiveTimelineRefresh(
|
||||||
@@ -847,13 +849,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (!alive()) return;
|
if (!alive()) return;
|
||||||
setFocusItem((currentItem) => {
|
setFocusItem((currentItem) => {
|
||||||
if (currentItem === focusItem) return undefined;
|
if (currentItem === focusItem) return undefined;
|
||||||
return currentItem;
|
return currentItem;
|
||||||
});
|
});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
}, [alive, focusItem, scrollToItem]);
|
}, [alive, focusItem, scrollToItem]);
|
||||||
|
|
||||||
// scroll to bottom of timeline
|
// scroll to bottom of timeline
|
||||||
|
|||||||
Reference in New Issue
Block a user