Compare commits

1 Commits

Author SHA1 Message Date
c264de53b5 Fix View updates on Android: bundled notes, portal stack, tap handling.
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 2s
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.
2026-08-23 08:38:23 +10:00
4 changed files with 56 additions and 19 deletions

View File

@@ -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. */

View File

@@ -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(
<div className={css.PortalLayer}>
<Overlay open backdrop={<OverlayBackdrop />}>
@@ -55,8 +57,7 @@ export function UpdatesDialog({
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: onClose,
clickOutsideDeactivates: true,
clickOutsideDeactivates: false,
escapeDeactivates: stopPropagation,
}}
>
@@ -181,6 +182,6 @@ export function UpdatesDialog({
</OverlayCenter>
</Overlay>
</div>,
document.body
portalTarget
);
}

View File

@@ -26,7 +26,10 @@ type UpdaterInfo = {
export function useOpenReleaseNotesDialog(): () => void {
const setDialogState = useSetAtom(releaseNotesDialogAtom);
return useCallback(() => {
// Defer so the opening tap does not trip FocusTrap click-outside on Android.
window.requestAnimationFrame(() => {
setDialogState({ open: true, manual: true });
});
}, [setDialogState]);
}

View File

@@ -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<string, string>;
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<UpdateManifest | null> {
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 null;
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)
}
try {
return loadBundledManifest();
} catch {
return null;
}
@@ -96,13 +126,15 @@ export async function loadUpdateDocument(file: string): Promise<ParsedUpdateDoc
try {
const response = await fetch(getUpdateFileUrl(safeFile), { cache: 'no-cache' });
if (!response.ok) return null;
if (response.ok) {
const source = await response.text();
return parseUpdateMarkdown(safeFile, source);
} catch {
return null;
}
} catch {
// fall through to bundled copy
}
return loadBundledDocument(safeFile);
}
export async function loadCurrentUpdateDocument(): Promise<ParsedUpdateDoc | null> {