From c264de53b5f2054de0b920c288c2581483e46974 Mon Sep 17 00:00:00 2001 From: litruv Date: Sun, 23 Aug 2026 08:38:23 +1000 Subject: [PATCH] Fix View updates on Android: bundled notes, portal stack, tap handling. Bundle update markdown at build time for Capacitor, portal into portalContainer above Settings, defer open to avoid FocusTrap closing on the same tap, and disable click-outside dismiss. --- .../updates-dialog/UpdatesDialog.css.ts | 1 + .../updates-dialog/UpdatesDialog.tsx | 9 +-- .../updates-dialog/UpdatesDialogHost.tsx | 5 +- src/app/data/updateNotes.ts | 60 ++++++++++++++----- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/app/components/updates-dialog/UpdatesDialog.css.ts b/src/app/components/updates-dialog/UpdatesDialog.css.ts index af1f358..c313196 100644 --- a/src/app/components/updates-dialog/UpdatesDialog.css.ts +++ b/src/app/components/updates-dialog/UpdatesDialog.css.ts @@ -7,6 +7,7 @@ export const PortalLayer = style({ position: 'fixed', inset: 0, zIndex: config.zIndex.Max, + pointerEvents: 'auto', }); /** Dialog + overlay padding must never exceed the viewport height. */ diff --git a/src/app/components/updates-dialog/UpdatesDialog.tsx b/src/app/components/updates-dialog/UpdatesDialog.tsx index 873363c..feecb10 100644 --- a/src/app/components/updates-dialog/UpdatesDialog.tsx +++ b/src/app/components/updates-dialog/UpdatesDialog.tsx @@ -4,7 +4,6 @@ import FocusTrap from 'focus-trap-react'; import { Box, Button, - config, Dialog, IconButton, Overlay, @@ -48,6 +47,9 @@ export function UpdatesDialog({ setShowOlderList(false); }; + const portalTarget = + document.getElementById('portalContainer') ?? document.body; + return createPortal(
}> @@ -55,8 +57,7 @@ export function UpdatesDialog({ @@ -181,6 +182,6 @@ export function UpdatesDialog({
, - document.body + portalTarget ); } diff --git a/src/app/components/updates-dialog/UpdatesDialogHost.tsx b/src/app/components/updates-dialog/UpdatesDialogHost.tsx index 14a9df9..8db2c91 100644 --- a/src/app/components/updates-dialog/UpdatesDialogHost.tsx +++ b/src/app/components/updates-dialog/UpdatesDialogHost.tsx @@ -26,7 +26,10 @@ type UpdaterInfo = { export function useOpenReleaseNotesDialog(): () => void { const setDialogState = useSetAtom(releaseNotesDialogAtom); return useCallback(() => { - setDialogState({ open: true, manual: true }); + // Defer so the opening tap does not trip FocusTrap click-outside on Android. + window.requestAnimationFrame(() => { + setDialogState({ open: true, manual: true }); + }); }, [setDialogState]); } diff --git a/src/app/data/updateNotes.ts b/src/app/data/updateNotes.ts index 497ab41..99b8f1e 100644 --- a/src/app/data/updateNotes.ts +++ b/src/app/data/updateNotes.ts @@ -1,5 +1,12 @@ +import bundledManifest from '../../../public/update/manifest.json'; import { trimTrailingSlash } from '../utils/common'; +const bundledMarkdownByFile = import.meta.glob('../../../public/update/*.md', { + query: '?raw', + import: 'default', + eager: true, +}) as Record; + function getUpdateBaseUrl(): string { const basePath = trimTrailingSlash(import.meta.env.BASE_URL || './'); const relative = @@ -64,9 +71,26 @@ function parseUpdateMarkdown(file: string, source: string): ParsedUpdateDoc { }; } +function loadBundledManifest(): UpdateManifest { + const manifest = bundledManifest as UpdateManifest; + return { + current: manifest.current, + older: (manifest.older ?? []).filter((entry) => entry?.file), + }; +} + +function loadBundledDocument(file: string): ParsedUpdateDoc | null { + const safeFile = file.replace(/^\/+/, ''); + const entry = Object.entries(bundledMarkdownByFile).find(([path]) => + path.endsWith(`/${safeFile}`) + ); + if (!entry) return null; + return parseUpdateMarkdown(safeFile, entry[1]); +} + export function resolveUpdateAssetUrl(src: string | undefined): string | undefined { if (!src) return undefined; - if (/^(https?:|data:|blob:)/i.test(src)) return src; + if (/^(https?:|data:|blob:|capacitor:)/i.test(src)) return src; if (src.startsWith('/')) return src; const normalized = src.replace(/^\.\//, ''); @@ -76,15 +100,21 @@ export function resolveUpdateAssetUrl(src: string | undefined): string | undefin export async function loadUpdateManifest(): Promise { try { const response = await fetch(getUpdateFileUrl('manifest.json'), { cache: 'no-cache' }); - if (!response.ok) return null; + if (response.ok) { + const data = (await response.json()) as UpdateManifest; + if (data?.current && Array.isArray(data.older)) { + return { + current: data.current, + older: data.older.filter((entry) => entry?.file), + }; + } + } + } catch { + // fall through to bundled copy (Capacitor / offline) + } - const data = (await response.json()) as UpdateManifest; - if (!data?.current || !Array.isArray(data.older)) return null; - - return { - current: data.current, - older: data.older.filter((entry) => entry?.file), - }; + try { + return loadBundledManifest(); } catch { return null; } @@ -96,13 +126,15 @@ export async function loadUpdateDocument(file: string): Promise {