feat(pwa): a relaunch also returns to the plan day it was on (#666)

This commit is contained in:
Maurice
2026-10-01 13:22:37 +02:00
parent d1f4deaa31
commit 99c62878f3
3 changed files with 43 additions and 9 deletions
@@ -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
+11 -1
View File
@@ -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()
})
})
+26 -6
View File
@@ -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 () => {