diff --git a/client/src/mobile/screens/trip/MTripShell.tsx b/client/src/mobile/screens/trip/MTripShell.tsx index 8daff7378..61cb96c17 100644 --- a/client/src/mobile/screens/trip/MTripShell.tsx +++ b/client/src/mobile/screens/trip/MTripShell.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState, type ComponentType, type ReactNode } from 'react' import { findFocusDayId, findTodayDayId } from '../../../components/Planner/today' +import { takeResumeDay } from '../../../utils/resumeRoute' import { CalendarCheck, CalendarDays, ChevronDown, ChevronLeft, Download, FileDown, FolderSync, List, Map as MapIcon, MoreHorizontal, Plane, Plus, Route, Rows3, SlidersHorizontal, Trash2, Upload, } from 'lucide-react' @@ -256,8 +257,11 @@ export default function MTripShell({ seededDayRef.current = true // Off the same helper file as the desktop day plan (#1567), so the two // cannot drift on what "today" means. - planner.tripActions.setSelectedDay(findFocusDayId(days) ?? days[0].id) - }, [isLoading, planner.selectedDayId, days, planner.tripActions]) + // A relaunch after the system closed the app goes back to the day it was on (#666). + const resumed = takeResumeDay(tripId) + const resumedDay = resumed != null && days.some(d => d.id === resumed) ? resumed : null + planner.tripActions.setSelectedDay(resumedDay ?? findFocusDayId(days) ?? days[0].id) + }, [isLoading, planner.selectedDayId, days, planner.tripActions, tripId]) // Swiping the day panel (#2051) can move the day well past the chips on // screen — the rail overflows from roughly six days on — so the active chip diff --git a/client/src/utils/resumeRoute.test.tsx b/client/src/utils/resumeRoute.test.tsx index 040ebc7e6..e92bc9ecd 100644 --- a/client/src/utils/resumeRoute.test.tsx +++ b/client/src/utils/resumeRoute.test.tsx @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest' import { renderHook } from '@testing-library/react' import { MemoryRouter } from 'react-router' import type { ReactNode } from 'react' -import { forgetResumeRoute, rememberRoute, RESUME_MAX_AGE_MS, takeResumeRoute, useRememberRoute } from './resumeRoute' +import { forgetResumeRoute, rememberRoute, RESUME_MAX_AGE_MS, takeResumeDay, takeResumeRoute, useRememberRoute } from './resumeRoute' const NOW = 1_800_000_000_000 @@ -58,4 +58,14 @@ describe('resume route (#1024)', () => { window.dispatchEvent(new Event('pagehide')) expect(JSON.parse(localStorage.getItem('trek:resume-route')!).path).toBe('/journey/9') }) + it('FE-UTIL-RESUME-666: the plan day comes back once, for its own trip only', () => { + rememberRoute('/trips/7', NOW, 55) + sessionStorage.clear() + takeResumeRoute(NOW, true) + expect(takeResumeDay(8)).toBeNull() + expect(takeResumeDay(7)).toBe(55) + expect(takeResumeDay(7)).toBeNull() + rememberRoute('/journey/1', NOW, 55) + expect(JSON.parse(localStorage.getItem('trek:resume-route')!).day).toBeUndefined() + }) }) diff --git a/client/src/utils/resumeRoute.ts b/client/src/utils/resumeRoute.ts index b4721780f..0194d081c 100644 --- a/client/src/utils/resumeRoute.ts +++ b/client/src/utils/resumeRoute.ts @@ -1,5 +1,6 @@ import { useEffect } from 'react' import { useLocation } from 'react-router' +import { useTripStore } from '../store/tripStore' /** * Where the installed app was when it went to the background (#1024). @@ -14,10 +15,27 @@ import { useLocation } from 'react-router' */ const KEY = 'trek:resume-route' const CHECKED_KEY = 'trek:resume-checked' +const DAY_KEY = 'trek:resume-day-' + +/** + * The day the plan was on when the app went away (#666), handed over once to + * whoever seeds the trip's first day selection. Null when there is none. + */ +export function takeResumeDay(tripId: number): number | null { + try { + const raw = sessionStorage.getItem(`${DAY_KEY}${tripId}`) + if (raw == null) return null + sessionStorage.removeItem(`${DAY_KEY}${tripId}`) + const day = Number(raw) + return Number.isInteger(day) ? day : null + } catch { + return null + } +} /** Older than this, a relaunch is a fresh start again. */ export const RESUME_MAX_AGE_MS = 6 * 60 * 60 * 1000 -interface ResumeRecord { path: string; tab?: string; at: number } +interface ResumeRecord { path: string; tab?: string; day?: number; at: number } /** True in a display mode that has no tab strip, i.e. launched from the home screen. */ export function isInstalledApp(): boolean { @@ -45,7 +63,7 @@ function withoutTabParam(path: string): string { } /** Called on every protected navigation and when the app is hidden. */ -export function rememberRoute(path: string, now = Date.now()): void { +export function rememberRoute(path: string, now = Date.now(), day: number | null = null): void { try { if (!resumable(path)) { localStorage.removeItem(KEY) @@ -57,7 +75,7 @@ export function rememberRoute(path: string, now = Date.now()): void { const asked = new URLSearchParams(path.split('?')[1] ?? '').get('tab') ?? undefined const tab = tripId ? sessionStorage.getItem(`trip-tab-${tripId}`) ?? asked : undefined const kept = tripId ? withoutTabParam(path) : path - const record: ResumeRecord = { path: kept, at: now, ...(tab ? { tab } : {}) } + const record: ResumeRecord = { path: kept, at: now, ...(tab ? { tab } : {}), ...(tripId && day != null ? { day } : {}) } localStorage.setItem(KEY, JSON.stringify(record)) } catch { // Storage refused: the relaunch simply starts where it always did. @@ -83,6 +101,7 @@ export function takeResumeRoute(now = Date.now(), installed = isInstalledApp()): if (now - record.at > RESUME_MAX_AGE_MS || !resumable(record.path)) return null const tripId = tripIdOf(record.path) if (tripId && record.tab) sessionStorage.setItem(`trip-tab-${tripId}`, record.tab) + if (tripId && typeof record.day === 'number') sessionStorage.setItem(`${DAY_KEY}${tripId}`, String(record.day)) return record.path } catch { return null @@ -102,9 +121,10 @@ export function useRememberRoute(): void { const location = useLocation() const path = location.pathname + location.search useEffect(() => { - rememberRoute(path) - const onVisibility = () => { if (document.visibilityState === 'hidden') rememberRoute(path) } - const onPageHide = () => rememberRoute(path) + const save = () => rememberRoute(path, Date.now(), useTripStore.getState().selectedDayId ?? null) + save() + const onVisibility = () => { if (document.visibilityState === 'hidden') save() } + const onPageHide = () => save() document.addEventListener('visibilitychange', onVisibility) window.addEventListener('pagehide', onPageHide) return () => {