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_COMPOSER_SNAP_INDEX = 2;
|
||||||
const BOTTOM_SHEET_FULL_SNAP_INDEX = 4;
|
const BOTTOM_SHEET_FULL_SNAP_INDEX = 4;
|
||||||
const OPENCODE_RUNTIME_DELIVERY_STATUS_REFRESH_DELAYS_MS = [15_000, 45_000, 90_000] as const;
|
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 {
|
interface MessagesPanelProps {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
|
|
@ -551,6 +552,8 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
const [messagesScrollTop, setMessagesScrollTop] = useState(
|
const [messagesScrollTop, setMessagesScrollTop] = useState(
|
||||||
initialSidebarStateRef.current.messagesScrollTop
|
initialSidebarStateRef.current.messagesScrollTop
|
||||||
);
|
);
|
||||||
|
const messagesScrollTopRef = useRef(initialSidebarStateRef.current.messagesScrollTop);
|
||||||
|
const messagesScrollPersistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const [bottomSheetSnapIndex, setBottomSheetSnapIndex] = useState(
|
const [bottomSheetSnapIndex, setBottomSheetSnapIndex] = useState(
|
||||||
initialSidebarStateRef.current.bottomSheetSnapIndex
|
initialSidebarStateRef.current.bottomSheetSnapIndex
|
||||||
);
|
);
|
||||||
|
|
@ -565,10 +568,43 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
setMessagesCollapsed(initialSidebarStateRef.current.messagesCollapsed);
|
setMessagesCollapsed(initialSidebarStateRef.current.messagesCollapsed);
|
||||||
setMessagesSearchBarVisible(initialSidebarStateRef.current.messagesSearchBarVisible);
|
setMessagesSearchBarVisible(initialSidebarStateRef.current.messagesSearchBarVisible);
|
||||||
setExpandedItemKey(initialSidebarStateRef.current.expandedItemKey);
|
setExpandedItemKey(initialSidebarStateRef.current.expandedItemKey);
|
||||||
|
messagesScrollTopRef.current = initialSidebarStateRef.current.messagesScrollTop;
|
||||||
setMessagesScrollTop(initialSidebarStateRef.current.messagesScrollTop);
|
setMessagesScrollTop(initialSidebarStateRef.current.messagesScrollTop);
|
||||||
setBottomSheetSnapIndex(initialSidebarStateRef.current.bottomSheetSnapIndex);
|
setBottomSheetSnapIndex(initialSidebarStateRef.current.bottomSheetSnapIndex);
|
||||||
}, [teamName]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
setTeamMessagesSidebarUiState(teamName, {
|
setTeamMessagesSidebarUiState(teamName, {
|
||||||
messagesSearchQuery,
|
messagesSearchQuery,
|
||||||
|
|
@ -1355,7 +1391,7 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
<div
|
<div
|
||||||
ref={sidebarScrollRef}
|
ref={sidebarScrollRef}
|
||||||
className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden pb-14 pr-3 pt-2"
|
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">
|
<div className="pl-3">
|
||||||
{defaultComposerSection}
|
{defaultComposerSection}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
MessagesPanel,
|
MessagesPanel,
|
||||||
reconcilePendingRepliesByMember,
|
reconcilePendingRepliesByMember,
|
||||||
} from '@renderer/components/team/messages/MessagesPanel';
|
} from '@renderer/components/team/messages/MessagesPanel';
|
||||||
|
import { setTeamMessagesSidebarUiState } from '@renderer/components/team/sidebar/teamSidebarUiState';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type { OpenCodeRuntimeDeliveryDebugDetails } from '@renderer/utils/openCodeRuntimeDeliveryDiagnostics';
|
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 () => {
|
it('hides passive peer summaries by default while unread badge only counts filtered unread messages', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue