Skip to content

Commit 09d27a6

Browse files
author
BuildTools
committed
Enhance view state management by integrating UiStore for scroll position and selected event persistence
addresses httptoolkit/httptoolkit#470 and further adds selected event persistence
1 parent 8d5550c commit 09d27a6

File tree

3 files changed

+570
-447
lines changed

3 files changed

+570
-447
lines changed

src/components/view/view-event-list.tsx

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
RTCConnection,
1818
TlsTunnel
1919
} from '../../types';
20+
import { UiStore } from '../../model/ui/ui-store';
2021

2122
import {
2223
getSummaryColor,
@@ -54,6 +55,7 @@ interface ViewEventListProps {
5455
isPaused: boolean;
5556

5657
contextMenuBuilder: ViewEventContextMenuBuilder;
58+
uiStore: UiStore;
5759

5860
moveSelection: (distance: number) => void;
5961
onSelected: (event: CollectedEvent | undefined) => void;
@@ -879,19 +881,37 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
879881
if (!listWindow) return true; // This means no rows, so we are effectively at the bottom
880882
else return (listWindow.scrollTop + SCROLL_BOTTOM_MARGIN) >= (listWindow.scrollHeight - listWindow.offsetHeight);
881883
}
882-
883884
private wasListAtBottom = true;
884885
private updateScrolledState = () => {
885886
requestAnimationFrame(() => { // Measure async, once the scroll has actually happened
886887
this.wasListAtBottom = this.isListAtBottom();
888+
889+
// Only save scroll position after we've restored the initial state
890+
if (this.hasRestoredInitialState) {
891+
const listWindow = this.listBodyRef.current?.parentElement;
892+
if (listWindow) {
893+
this.props.uiStore.setViewScrollPosition(listWindow.scrollTop);
894+
}
895+
}
887896
});
888897
}
889898

899+
private hasRestoredInitialState = false;
890900
componentDidMount() {
891-
this.updateScrolledState();
901+
// Don't save scroll state immediately - wait until we've restored first
902+
903+
// Use a more aggressive delay to ensure DOM is fully ready
904+
setTimeout(() => {
905+
this.restoreScrollPosition();
906+
907+
// Only start tracking scroll changes after we've restored
908+
setTimeout(() => {
909+
this.hasRestoredInitialState = true;
910+
}, 100);
911+
}, 100);
892912
}
893913

894-
componentDidUpdate() {
914+
componentDidUpdate(prevProps: ViewEventListProps) {
895915
if (this.listBodyRef.current?.parentElement?.contains(document.activeElement)) {
896916
// If we previously had something here focused, and we've updated, update
897917
// the focus too, to make sure it's in the right place.
@@ -901,7 +921,29 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
901921
// If we previously were scrolled to the bottom of the list, but now we're not,
902922
// scroll there again ourselves now.
903923
if (this.wasListAtBottom && !this.isListAtBottom()) {
904-
this.listRef.current?.scrollToItem(this.props.events.length - 1);
924+
this.listRef.current?.scrollToItem(this.props.events.length - 1);
925+
} else if (prevProps.selectedEvent !== this.props.selectedEvent && this.props.selectedEvent) {
926+
// If the selected event changed and we have a selected event, scroll to it
927+
// This handles restoring the selected event when returning to the tab
928+
this.scrollToEvent(this.props.selectedEvent);
929+
} else if (prevProps.filteredEvents.length !== this.props.filteredEvents.length) {
930+
// If the filtered events changed (e.g., new events loaded), try to restore scroll position
931+
setTimeout(() => {
932+
this.restoreScrollPosition();
933+
}, 50);
934+
}
935+
}
936+
937+
private restoreScrollPosition = () => {
938+
// Only restore if we have a saved position
939+
const savedPosition = this.props.uiStore.viewScrollPosition;
940+
if (savedPosition > 0) {
941+
const listWindow = this.listBodyRef.current?.parentElement;
942+
if (listWindow) { // Only restore if we're not close to the current position (avoid unnecessary scrolling)
943+
if (Math.abs(listWindow.scrollTop - savedPosition) > 10) {
944+
listWindow.scrollTop = savedPosition;
945+
}
946+
}
905947
}
906948
}
907949

@@ -1005,5 +1047,12 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
10051047
}
10061048

10071049
event.preventDefault();
1050+
} // Public method to force scroll and selection restoration
1051+
public restoreViewState = () => {
1052+
if (this.props.selectedEvent) {
1053+
this.scrollToEvent(this.props.selectedEvent);
1054+
} else {
1055+
this.restoreScrollPosition();
1056+
}
10081057
}
10091058
}

src/components/view/view-page.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,13 @@ class ViewPage extends React.Component<ViewPageProps> {
207207
filteredEventCount: [filteredEvents.length, events.length]
208208
};
209209
}
210-
211210
@computed
212211
get selectedEvent() {
212+
// First try to use the URL-based eventId, then fallback to the persisted selection
213+
const targetEventId = this.props.eventId || this.props.uiStore.selectedEventId;
214+
213215
return _.find(this.props.eventsStore.events, {
214-
id: this.props.eventId
216+
id: targetEventId
215217
});
216218
}
217219

@@ -240,12 +242,16 @@ class ViewPage extends React.Component<ViewPageProps> {
240242
this.onBuildRuleFromExchange,
241243
this.onPrepareToResendRequest
242244
);
243-
244245
componentDidMount() {
245246
// After first render, scroll to the selected event (or the end of the list) by default:
246247
requestAnimationFrame(() => {
247248
if (this.props.eventId && this.selectedEvent) {
248249
this.onScrollToCenterEvent(this.selectedEvent);
250+
} else if (!this.props.eventId && this.props.uiStore.selectedEventId) {
251+
// If no URL eventId but we have a persisted selection, restore it
252+
setTimeout(() => {
253+
this.listRef.current?.restoreViewState();
254+
}, 100);
249255
} else {
250256
this.onScrollToEnd();
251257
}
@@ -327,6 +333,18 @@ class ViewPage extends React.Component<ViewPageProps> {
327333
})
328334
);
329335
}
336+
componentWillUnmount() {
337+
// Component is unmounting
338+
}
339+
340+
componentDidUpdate(prevProps: ViewPageProps) {
341+
// Only clear persisted selection if we're explicitly navigating to a different event via URL
342+
// Don't clear it when going from eventId to no eventId (which happens when clearing selection)
343+
if (this.props.eventId && prevProps.eventId && this.props.eventId !== prevProps.eventId) {
344+
// Clear persisted selection only when explicitly navigating between different events via URL
345+
this.props.uiStore.setSelectedEventId(undefined);
346+
}
347+
}
330348

331349
isSendAvailable() {
332350
return versionSatisfies(serverVersion.value as string, SERVER_SEND_API_SUPPORTED);
@@ -447,8 +465,8 @@ class ViewPage extends React.Component<ViewPageProps> {
447465

448466
moveSelection={this.moveSelection}
449467
onSelected={this.onSelected}
450-
451468
contextMenuBuilder={this.contextMenuBuilder}
469+
uiStore={this.props.uiStore}
452470

453471
ref={this.listRef}
454472
/>
@@ -488,9 +506,11 @@ class ViewPage extends React.Component<ViewPageProps> {
488506
onSearchFiltersConsidered(filters: FilterSet | undefined) {
489507
this.searchFiltersUnderConsideration = filters;
490508
}
491-
492509
@action.bound
493510
onSelected(event: CollectedEvent | undefined) {
511+
// Persist the selected event to UiStore for tab switching
512+
this.props.uiStore.setSelectedEventId(event?.id);
513+
494514
this.props.navigate(event
495515
? `/view/${event.id}`
496516
: '/view'

0 commit comments

Comments
 (0)