Compare commits

1 Commits

Author SHA1 Message Date
a20c871726 Fix updates dialog on Android by portaling above Settings and resolving Capacitor asset URLs.
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 2s
2026-08-23 08:19:51 +10:00
5 changed files with 39 additions and 19 deletions

View File

@@ -9,7 +9,7 @@
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "vite build", "build": "node ../scripts/generate-update-manifest.mjs && vite build",
"lint": "yarn check:eslint && yarn check:prettier", "lint": "yarn check:eslint && yarn check:prettier",
"check:eslint": "eslint src/*", "check:eslint": "eslint src/*",
"check:prettier": "prettier --check .", "check:prettier": "prettier --check .",

View File

@@ -3,7 +3,13 @@ import { color, config, toRem } from 'folds';
const MOBILE_BREAKPOINT = '480px'; const MOBILE_BREAKPOINT = '480px';
/** Dialog height cap — 85% of the viewport, minus safe areas. */ export const PortalLayer = style({
position: 'fixed',
inset: 0,
zIndex: config.zIndex.Max,
});
/** Dialog + overlay padding must never exceed the viewport height. */
const DIALOG_MAX_HEIGHT = const DIALOG_MAX_HEIGHT =
'calc(85vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))'; 'calc(85vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))';

View File

@@ -1,8 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { import {
Box, Box,
Button, Button,
config,
Dialog, Dialog,
IconButton, IconButton,
Overlay, Overlay,
@@ -46,18 +48,19 @@ export function UpdatesDialog({
setShowOlderList(false); setShowOlderList(false);
}; };
return ( return createPortal(
<Overlay open backdrop={<OverlayBackdrop />}> <div className={css.PortalLayer}>
<OverlayCenter className={css.OverlayFrame}> <Overlay open backdrop={<OverlayBackdrop />}>
<FocusTrap <OverlayCenter className={css.OverlayFrame}>
focusTrapOptions={{ <FocusTrap
initialFocus: false, focusTrapOptions={{
onDeactivate: onClose, initialFocus: false,
clickOutsideDeactivates: true, onDeactivate: onClose,
escapeDeactivates: stopPropagation, clickOutsideDeactivates: true,
}} escapeDeactivates: stopPropagation,
> }}
<Dialog variant="Surface" className={css.DialogShell}> >
<Dialog variant="Surface" className={css.DialogShell}>
<Box className={css.Hero}> <Box className={css.Hero}>
<Box className={css.HeroRow}> <Box className={css.HeroRow}>
<img className={css.HeroLogo} src={PaarrotSVG} alt="" draggable={false} /> <img className={css.HeroLogo} src={PaarrotSVG} alt="" draggable={false} />
@@ -177,5 +180,7 @@ export function UpdatesDialog({
</FocusTrap> </FocusTrap>
</OverlayCenter> </OverlayCenter>
</Overlay> </Overlay>
</div>,
document.body
); );
} }

View File

@@ -1,6 +1,15 @@
import { trimTrailingSlash } from '../utils/common'; import { trimTrailingSlash } from '../utils/common';
const UPDATE_BASE_PATH = `${trimTrailingSlash(import.meta.env.BASE_URL)}/update`; function getUpdateBaseUrl(): string {
const basePath = trimTrailingSlash(import.meta.env.BASE_URL || './');
const relative =
basePath === '.' || basePath === '' ? 'update/' : `${basePath}/update/`;
return new URL(relative, window.location.href).href.replace(/\/$/, '');
}
function getUpdateFileUrl(file: string): string {
return new URL(file, `${getUpdateBaseUrl()}/`).href;
}
export type UpdateManifestEntry = { export type UpdateManifestEntry = {
file: string; file: string;
@@ -61,12 +70,12 @@ export function resolveUpdateAssetUrl(src: string | undefined): string | undefin
if (src.startsWith('/')) return src; if (src.startsWith('/')) return src;
const normalized = src.replace(/^\.\//, ''); const normalized = src.replace(/^\.\//, '');
return `${UPDATE_BASE_PATH}/${normalized}`; return new URL(normalized, `${getUpdateBaseUrl()}/`).href;
} }
export async function loadUpdateManifest(): Promise<UpdateManifest | null> { export async function loadUpdateManifest(): Promise<UpdateManifest | null> {
try { try {
const response = await fetch(`${UPDATE_BASE_PATH}/manifest.json`, { cache: 'no-cache' }); const response = await fetch(getUpdateFileUrl('manifest.json'), { cache: 'no-cache' });
if (!response.ok) return null; if (!response.ok) return null;
const data = (await response.json()) as UpdateManifest; const data = (await response.json()) as UpdateManifest;
@@ -86,7 +95,7 @@ export async function loadUpdateDocument(file: string): Promise<ParsedUpdateDoc
if (!safeFile || safeFile.includes('..')) return null; if (!safeFile || safeFile.includes('..')) return null;
try { try {
const response = await fetch(`${UPDATE_BASE_PATH}/${safeFile}`, { cache: 'no-cache' }); const response = await fetch(getUpdateFileUrl(safeFile), { cache: 'no-cache' });
if (!response.ok) return null; if (!response.ok) return null;
const source = await response.text(); const source = await response.text();

View File

@@ -699,10 +699,10 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
<MessageNotifications /> <MessageNotifications />
<BackgroundSyncSetup /> <BackgroundSyncSetup />
<PaarrotAPIInitializer /> <PaarrotAPIInitializer />
<UpdatesDialogHost />
<TaskbarFlashStopper /> <TaskbarFlashStopper />
<AndroidShareIntentHandler /> <AndroidShareIntentHandler />
{children} {children}
<UpdatesDialogHost />
</> </>
); );
} }