diff --git a/src/components/view/view-event-list.tsx b/src/components/view/view-event-list.tsx index 3393d425..3adf4d55 100644 --- a/src/components/view/view-event-list.tsx +++ b/src/components/view/view-event-list.tsx @@ -17,6 +17,7 @@ import { RTCConnection, TlsTunnel } from '../../types'; +import { UiStore } from '../../model/ui/ui-store'; import { getSummaryColor, @@ -54,6 +55,7 @@ interface ViewEventListProps { isPaused: boolean; contextMenuBuilder: ViewEventContextMenuBuilder; + uiStore: UiStore; moveSelection: (distance: number) => void; onSelected: (event: CollectedEvent | undefined) => void; @@ -767,6 +769,17 @@ export class ViewEventList extends React.Component { private listBodyRef = React.createRef(); private listRef = React.createRef(); + private setListBodyRef = (element: HTMLDivElement | null) => { + // Update the ref + (this.listBodyRef as any).current = element; + + // If the element is being mounted and we haven't restored state yet, do it now + if (element && !this.hasRestoredScrollState) { + this.restoreScrollPosition(); + this.hasRestoredScrollState = true; + } + }; + private KeyBoundListWindow = observer( React.forwardRef( (props: any, ref) =>
{ : {({ height, width }) => {() => { } private wasListAtBottom = true; + private updateScrolledState = () => { requestAnimationFrame(() => { // Measure async, once the scroll has actually happened this.wasListAtBottom = this.isListAtBottom(); + + // Only save scroll position after we've restored the initial state + if (this.hasRestoredScrollState) { + const listWindow = this.listBodyRef.current?.parentElement; + + const scrollPosition = this.wasListAtBottom + ? 'end' + : (listWindow?.scrollTop ?? 'end'); + if (listWindow) { + this.props.uiStore.setViewScrollPosition(scrollPosition); + } + } }); } - componentDidMount() { - this.updateScrolledState(); - } + private hasRestoredScrollState = false; - componentDidUpdate() { + componentDidUpdate(prevProps: ViewEventListProps) { if (this.listBodyRef.current?.parentElement?.contains(document.activeElement)) { // If we previously had something here focused, and we've updated, update // the focus too, to make sure it's in the right place. @@ -902,6 +926,25 @@ export class ViewEventList extends React.Component { // scroll there again ourselves now. if (this.wasListAtBottom && !this.isListAtBottom()) { this.listRef.current?.scrollToItem(this.props.events.length - 1); + } else if (prevProps.selectedEvent !== this.props.selectedEvent && this.props.selectedEvent) { + // If the selected event changed and we have a selected event, scroll to it + // This handles restoring the selected event when returning to the tab + this.scrollToEvent(this.props.selectedEvent); + } + } + + private restoreScrollPosition = () => { + const savedPosition = this.props.uiStore.viewScrollPosition; + const listWindow = this.listBodyRef.current?.parentElement; + if (listWindow) { + if (savedPosition === 'end') { + listWindow.scrollTop = listWindow.scrollHeight; + } else { + // Only restore if we're not close to the current position (avoid unnecessary scrolling) + if (Math.abs(listWindow.scrollTop - savedPosition) > 10) { + listWindow.scrollTop = savedPosition; + } + } } } diff --git a/src/components/view/view-page.tsx b/src/components/view/view-page.tsx index c1618c20..cbb4d4f7 100644 --- a/src/components/view/view-page.tsx +++ b/src/components/view/view-page.tsx @@ -207,11 +207,13 @@ class ViewPage extends React.Component { filteredEventCount: [filteredEvents.length, events.length] }; } - @computed get selectedEvent() { + // First try to use the URL-based eventId, then fallback to the persisted selection + const targetEventId = this.props.eventId || this.props.uiStore.selectedEventId; + return _.find(this.props.eventsStore.events, { - id: this.props.eventId + id: targetEventId }); } @@ -242,12 +244,11 @@ class ViewPage extends React.Component { ); componentDidMount() { - // After first render, scroll to the selected event (or the end of the list) by default: + // After first render, if we're jumping to an event, then scroll to it: requestAnimationFrame(() => { if (this.props.eventId && this.selectedEvent) { + this.props.uiStore.setSelectedEventId(this.props.eventId); this.onScrollToCenterEvent(this.selectedEvent); - } else { - this.onScrollToEnd(); } }); @@ -328,6 +329,15 @@ class ViewPage extends React.Component { ); } + componentDidUpdate(prevProps: ViewPageProps) { + // Only clear persisted selection if we're explicitly navigating to a different event via URL + // Don't clear it when going from eventId to no eventId (which happens when clearing selection) + if (this.props.eventId && prevProps.eventId && this.props.eventId !== prevProps.eventId) { + // Clear persisted selection only when explicitly navigating between different events via URL + this.props.uiStore.setSelectedEventId(undefined); + } + } + isSendAvailable() { return versionSatisfies(serverVersion.value as string, SERVER_SEND_API_SUPPORTED); } @@ -447,8 +457,8 @@ class ViewPage extends React.Component { moveSelection={this.moveSelection} onSelected={this.onSelected} - contextMenuBuilder={this.contextMenuBuilder} + uiStore={this.props.uiStore} ref={this.listRef} /> @@ -491,6 +501,8 @@ class ViewPage extends React.Component { @action.bound onSelected(event: CollectedEvent | undefined) { + this.props.uiStore.setSelectedEventId(event?.id); + this.props.navigate(event ? `/view/${event.id}` : '/view' diff --git a/src/model/ui/ui-store.ts b/src/model/ui/ui-store.ts index 8a40120a..607eb89b 100644 --- a/src/model/ui/ui-store.ts +++ b/src/model/ui/ui-store.ts @@ -251,6 +251,12 @@ export class UiStore { @observable expandedViewCard: ExpandableViewCardKey | undefined; + @observable + viewScrollPosition: number | 'end' = 'end'; + + @observable + selectedEventId: string | undefined; + @computed get viewCardProps() { return _.mapValues(this.viewCardStates, (state, key) => ({ @@ -436,6 +442,17 @@ export class UiStore { @persist @observable exportSnippetFormat: string | undefined; + // Actions for persisting view state when switching tabs + @action.bound + setViewScrollPosition(position: number | 'end') { + this.viewScrollPosition = position; + } + + @action.bound + setSelectedEventId(eventId: string | undefined) { + this.selectedEventId = eventId; + } + /** * This tracks the context menu state *only if it's not handled natively*. This state * is rendered by React as a fallback when DesktopApi.openContextMenu is not available.