perf: debounce messages scroll persistence
This commit is contained in:
parent
fa3f8ce85c
commit
3c37b22379
2 changed files with 100 additions and 1 deletions
|
|
@ -87,6 +87,7 @@ const BOTTOM_SHEET_COLLAPSED_SNAP_INDEX = 1;
|
|||
const BOTTOM_SHEET_COMPOSER_SNAP_INDEX = 2;
|
||||
const BOTTOM_SHEET_FULL_SNAP_INDEX = 4;
|
||||
const OPENCODE_RUNTIME_DELIVERY_STATUS_REFRESH_DELAYS_MS = [15_000, 45_000, 90_000] as const;
|
||||
const MESSAGES_SCROLL_TOP_PERSIST_DELAY_MS = 100;
|
||||
|
||||
interface MessagesPanelProps {
|
||||
teamName: string;
|
||||
|
|
@ -551,6 +552,8 @@ export const MessagesPanel = memo(function MessagesPanel({
|
|||
const [messagesScrollTop, setMessagesScrollTop] = useState(
|
||||
initialSidebarStateRef.current.messagesScrollTop
|
||||
);
|
||||
const messagesScrollTopRef = useRef(initialSidebarStateRef.current.messagesScrollTop);
|
||||
const messagesScrollPersistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [bottomSheetSnapIndex, setBottomSheetSnapIndex] = useState(
|
||||
initialSidebarStateRef.current.bottomSheetSnapIndex
|
||||
);
|
||||
|
|
@ -565,10 +568,43 @@ export const MessagesPanel = memo(function MessagesPanel({
|
|||
setMessagesCollapsed(initialSidebarStateRef.current.messagesCollapsed);
|
||||
setMessagesSearchBarVisible(initialSidebarStateRef.current.messagesSearchBarVisible);
|
||||
setExpandedItemKey(initialSidebarStateRef.current.expandedItemKey);
|
||||
messagesScrollTopRef.current = initialSidebarStateRef.current.messagesScrollTop;
|
||||
setMessagesScrollTop(initialSidebarStateRef.current.messagesScrollTop);
|
||||
setBottomSheetSnapIndex(initialSidebarStateRef.current.bottomSheetSnapIndex);
|
||||
}, [teamName]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (messagesScrollPersistTimerRef.current) {
|
||||
clearTimeout(messagesScrollPersistTimerRef.current);
|
||||
messagesScrollPersistTimerRef.current = null;
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const persistMessagesScrollTop = useCallback((nextScrollTop: number): void => {
|
||||
messagesScrollTopRef.current = nextScrollTop;
|
||||
if (messagesScrollPersistTimerRef.current) {
|
||||
clearTimeout(messagesScrollPersistTimerRef.current);
|
||||
}
|
||||
messagesScrollPersistTimerRef.current = setTimeout(() => {
|
||||
messagesScrollPersistTimerRef.current = null;
|
||||
setMessagesScrollTop((current) =>
|
||||
Math.abs(current - messagesScrollTopRef.current) < 1
|
||||
? current
|
||||
: messagesScrollTopRef.current
|
||||
);
|
||||
}, MESSAGES_SCROLL_TOP_PERSIST_DELAY_MS);
|
||||
}, []);
|
||||
|
||||
const handleSidebarScroll = useCallback(
|
||||
(event: React.UIEvent<HTMLDivElement>): void => {
|
||||
persistMessagesScrollTop(event.currentTarget.scrollTop);
|
||||
},
|
||||
[persistMessagesScrollTop]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTeamMessagesSidebarUiState(teamName, {
|
||||
messagesSearchQuery,
|
||||
|
|
@ -1355,7 +1391,7 @@ export const MessagesPanel = memo(function MessagesPanel({
|
|||
<div
|
||||
ref={sidebarScrollRef}
|
||||
className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden pb-14 pr-3 pt-2"
|
||||
onScroll={(e) => setMessagesScrollTop(e.currentTarget.scrollTop)}
|
||||
onScroll={handleSidebarScroll}
|
||||
>
|
||||
<div className="pl-3">
|
||||
{defaultComposerSection}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
MessagesPanel,
|
||||
reconcilePendingRepliesByMember,
|
||||
} from '@renderer/components/team/messages/MessagesPanel';
|
||||
import { setTeamMessagesSidebarUiState } from '@renderer/components/team/sidebar/teamSidebarUiState';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { OpenCodeRuntimeDeliveryDebugDetails } from '@renderer/utils/openCodeRuntimeDeliveryDiagnostics';
|
||||
|
|
@ -333,6 +334,68 @@ describe('MessagesPanel idle summary invariants', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('persists sidebar scroll position after scroll settles', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
storeState.teamMessagesByName['atlas-hq'] = {
|
||||
canonicalMessages: [makeMessage({ messageId: 'm-1', text: 'hello' })],
|
||||
optimisticMessages: [],
|
||||
feedRevision: 'rev-1',
|
||||
nextCursor: null,
|
||||
hasMore: false,
|
||||
lastFetchedAt: Date.now(),
|
||||
loadingHead: false,
|
||||
loadingOlder: false,
|
||||
headHydrated: true,
|
||||
};
|
||||
root.render(
|
||||
React.createElement(MessagesPanel, {
|
||||
teamName: 'atlas-hq',
|
||||
position: 'sidebar',
|
||||
onPositionChange: vi.fn(),
|
||||
members: [],
|
||||
tasks: [],
|
||||
timeWindow: null,
|
||||
pendingRepliesByMember: {},
|
||||
onPendingReplyChange: vi.fn(),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
vi.mocked(setTeamMessagesSidebarUiState).mockClear();
|
||||
const scrollContainer = host.querySelector('.overflow-y-auto') as HTMLDivElement | null;
|
||||
expect(scrollContainer).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
scrollContainer!.scrollTop = 320;
|
||||
scrollContainer!.dispatchEvent(new Event('scroll', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(setTeamMessagesSidebarUiState).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(100);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(setTeamMessagesSidebarUiState).toHaveBeenCalledWith(
|
||||
'atlas-hq',
|
||||
expect.objectContaining({ messagesScrollTop: 320 })
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('hides passive peer summaries by default while unread badge only counts filtered unread messages', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
|
|
|
|||
Loading…
Reference in a new issue