diff --git a/mobile/src/browser/MobileBrowserPane.tsx b/mobile/src/browser/MobileBrowserPane.tsx index 0771c2a3643..787e069618f 100644 --- a/mobile/src/browser/MobileBrowserPane.tsx +++ b/mobile/src/browser/MobileBrowserPane.tsx @@ -22,6 +22,11 @@ import { } from './mobile-browser-frame-state' import { displayBrowserUrl, normalizeBrowserUrl } from './browser-url' import { resolveMobileBrowserAddressSync } from './mobile-browser-address-sync' +import { + resolveMobileBrowserNavigationState, + type MobileBrowserNavigationRefinement, + type MobileBrowserNavigationState +} from './mobile-browser-navigation-state' import { MobileBrowserPaneView } from './MobileBrowserPaneView' import { useMobileBrowserInteractions } from './use-mobile-browser-interactions' import { useMobileBrowserPaneLayers } from './use-mobile-browser-pane-layers' @@ -84,10 +89,21 @@ export function MobileBrowserPane({ const cachedInitialFrame = peekCachedBrowserFrame(cacheKey) const [addressValue, setAddressValue] = useState(displayBrowserUrl(tab.url)) const [addressFocused, setAddressFocused] = useState(false) - const [navigationState, setNavigationState] = useState({ - canGoBack: tab.canGoBack, - canGoForward: tab.canGoForward - }) + // A host `navigation` event refines the tab's navigability until the tab itself republishes; + // an older host that never emits it leaves the tab props in charge. Derived in render so a tab + // update never lags one commit behind. + const [navigationRefinement, setNavigationRefinement] = + useState(null) + const navigationState = resolveMobileBrowserNavigationState(tab, navigationRefinement) + const setNavigationState = useCallback( + (next: MobileBrowserNavigationState) => { + setNavigationRefinement({ + ...next, + base: { canGoBack: tab.canGoBack, canGoForward: tab.canGoForward } + }) + }, + [tab.canGoBack, tab.canGoForward] + ) const [addressSyncState, setAddressSyncState] = useState({ focused: false, url: tab.url @@ -177,12 +193,6 @@ export function MobileBrowserPane({ focused: addressFocused, url: tab.url }) - // The screencast `navigation` event is a newer-host refinement; the tab props stay - // the baseline so an older host that never emits it does not freeze Back/Forward. - useEffect(() => { - setNavigationState({ canGoBack: tab.canGoBack, canGoForward: tab.canGoForward }) - }, [tab.canGoBack, tab.canGoForward]) - useEffect(() => { if (addressSync.nextState === addressSyncState) { return diff --git a/mobile/src/browser/mobile-browser-navigation-state.ts b/mobile/src/browser/mobile-browser-navigation-state.ts new file mode 100644 index 00000000000..2a7b446ecdb --- /dev/null +++ b/mobile/src/browser/mobile-browser-navigation-state.ts @@ -0,0 +1,22 @@ +export type MobileBrowserNavigationState = { canGoBack: boolean; canGoForward: boolean } + +/** A host `navigation` refinement plus the tab props it refined, so a later tab update wins. */ +export type MobileBrowserNavigationRefinement = MobileBrowserNavigationState & { + base: MobileBrowserNavigationState +} + +// Why: the tab props are the baseline every host publishes; the screencast `navigation` event is +// a newer-host refinement that only stands until the tab itself republishes navigability. +export function resolveMobileBrowserNavigationState( + tab: MobileBrowserNavigationState, + refinement: MobileBrowserNavigationRefinement | null +): MobileBrowserNavigationState { + if ( + refinement && + refinement.base.canGoBack === tab.canGoBack && + refinement.base.canGoForward === tab.canGoForward + ) { + return { canGoBack: refinement.canGoBack, canGoForward: refinement.canGoForward } + } + return { canGoBack: tab.canGoBack, canGoForward: tab.canGoForward } +} diff --git a/mobile/src/browser/use-mobile-browser-stream.ts b/mobile/src/browser/use-mobile-browser-stream.ts index e44d061969f..21aab6338d5 100644 --- a/mobile/src/browser/use-mobile-browser-stream.ts +++ b/mobile/src/browser/use-mobile-browser-stream.ts @@ -60,7 +60,7 @@ type MobileBrowserStreamArgs = { setError: Dispatch> setFrameMetadata: Dispatch> setFrameUri: Dispatch> - setNavigationState?: Dispatch> + setNavigationState?: (next: { canGoBack: boolean; canGoForward: boolean }) => void setZoom: Dispatch> streamGenerationRef: { current: number } tab: MobileBrowserTab diff --git a/mobile/src/components/MobileSyntaxSegments.tsx b/mobile/src/components/MobileSyntaxSegments.tsx index 8f63e4bdade..0da9fe1d438 100644 --- a/mobile/src/components/MobileSyntaxSegments.tsx +++ b/mobile/src/components/MobileSyntaxSegments.tsx @@ -6,9 +6,8 @@ export function MobileSyntaxSegments({ segments }: { segments: MobileSyntaxSegme let sourceOffset = 0 return ( <> - {segments.map((segment, index) => { - // The index keeps two adjacent zero-length segments of the same kind distinct. - const key = `${index}:${sourceOffset}:${segment.kind}` + {segments.map((segment) => { + const key = `${sourceOffset}:${segment.kind}` sourceOffset += segment.text.length return (