Compare commits
5 Commits
e65a516350
...
7d866404b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d866404b3 | |||
| c264de53b5 | |||
| b6f3f5c0aa | |||
| 25be638c5e | |||
| a20c871726 |
@@ -96,6 +96,12 @@
|
|||||||
</script>
|
</script>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<div id="portalContainer"></div>
|
<div id="portalContainer"></div>
|
||||||
|
<style>
|
||||||
|
#portalContainer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script type="module" src="./src/index.tsx"></script>
|
<script type="module" src="./src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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 .",
|
||||||
|
|||||||
114
scripts/generate-update-manifest.mjs
Normal file
114
scripts/generate-update-manifest.mjs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Regenerates public/update/manifest.json from markdown files.
|
||||||
|
*
|
||||||
|
* - currentupdate.md is always the "current" entry
|
||||||
|
* - Other *.md files matching X.Y.Z.md become "older" (sorted newest first)
|
||||||
|
* - title, date, version come from YAML frontmatter when present
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
const UPDATE_DIR = path.join(ROOT, 'public', 'update');
|
||||||
|
const MANIFEST_PATH = path.join(UPDATE_DIR, 'manifest.json');
|
||||||
|
const CURRENT_FILE = 'currentupdate.md';
|
||||||
|
const VERSION_FILE_RE = /^(\d+\.\d+\.\d+)\.md$/;
|
||||||
|
|
||||||
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
||||||
|
|
||||||
|
function parseFrontmatter(source) {
|
||||||
|
const match = FRONTMATTER_RE.exec(source.trimStart());
|
||||||
|
if (!match) return {};
|
||||||
|
|
||||||
|
const frontmatter = {};
|
||||||
|
match[1].split('\n').forEach((line) => {
|
||||||
|
const colon = line.indexOf(':');
|
||||||
|
if (colon <= 0) return;
|
||||||
|
const key = line.slice(0, colon).trim();
|
||||||
|
const raw = line.slice(colon + 1).trim();
|
||||||
|
frontmatter[key] = raw.replace(/^['"]|['"]$/g, '');
|
||||||
|
});
|
||||||
|
return frontmatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersions(a, b) {
|
||||||
|
const pa = a.split('.').map((part) => Number.parseInt(part, 10));
|
||||||
|
const pb = b.split('.').map((part) => Number.parseInt(part, 10));
|
||||||
|
const len = Math.max(pa.length, pb.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
const na = pa[i] ?? 0;
|
||||||
|
const nb = pb[i] ?? 0;
|
||||||
|
if (na !== nb) return nb - na;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMarkdownMeta(fileName) {
|
||||||
|
const filePath = path.join(UPDATE_DIR, fileName);
|
||||||
|
const source = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const frontmatter = parseFrontmatter(source);
|
||||||
|
const versionFromName = VERSION_FILE_RE.exec(fileName)?.[1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
file: fileName,
|
||||||
|
version: frontmatter.version ?? versionFromName,
|
||||||
|
title: frontmatter.title,
|
||||||
|
date: frontmatter.date,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildManifest() {
|
||||||
|
if (!fs.existsSync(UPDATE_DIR)) {
|
||||||
|
throw new Error(`Update directory not found: ${UPDATE_DIR}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPath = path.join(UPDATE_DIR, CURRENT_FILE);
|
||||||
|
if (!fs.existsSync(currentPath)) {
|
||||||
|
throw new Error(`Missing ${CURRENT_FILE} in ${UPDATE_DIR}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const older = fs
|
||||||
|
.readdirSync(UPDATE_DIR, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isFile() && VERSION_FILE_RE.test(entry.name))
|
||||||
|
.map((entry) => readMarkdownMeta(entry.name))
|
||||||
|
.filter((entry) => entry.version)
|
||||||
|
.sort((a, b) => compareVersions(a.version, b.version));
|
||||||
|
|
||||||
|
return {
|
||||||
|
current: CURRENT_FILE,
|
||||||
|
older: older.map(({ file, version, title, date }) => {
|
||||||
|
const item = { file };
|
||||||
|
if (version) item.version = version;
|
||||||
|
if (title) item.title = title;
|
||||||
|
if (date) item.date = date;
|
||||||
|
return item;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const manifest = buildManifest();
|
||||||
|
const json = `${JSON.stringify(manifest, null, 2)}\n`;
|
||||||
|
|
||||||
|
let previous = null;
|
||||||
|
if (fs.existsSync(MANIFEST_PATH)) {
|
||||||
|
previous = fs.readFileSync(MANIFEST_PATH, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous === json) {
|
||||||
|
console.log('[update-manifest] manifest.json is already up to date');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(MANIFEST_PATH, json, 'utf8');
|
||||||
|
console.log(
|
||||||
|
`[update-manifest] wrote manifest.json (${manifest.older.length} older version(s))`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { ReactNode, useCallback, useState } from 'react';
|
import React, { ReactNode, useCallback, useState } from 'react';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { Modal, Overlay, OverlayBackdrop, OverlayCenter, PopOutContainerProvider } from 'folds';
|
import { Modal, Overlay, OverlayBackdrop, OverlayCenter, PopOutContainerProvider } from 'folds';
|
||||||
import { stopPropagation } from '../utils/keyboard';
|
import { stopPropagation } from '../utils/keyboard';
|
||||||
|
import { releaseNotesDialogAtom } from '../state/releaseNotes';
|
||||||
|
|
||||||
type Modal500Props = {
|
type Modal500Props = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
@@ -10,11 +12,13 @@ type Modal500Props = {
|
|||||||
export function Modal500({ requestClose, children }: Modal500Props) {
|
export function Modal500({ requestClose, children }: Modal500Props) {
|
||||||
const [modalEl, setModalEl] = useState<HTMLDivElement | null>(null);
|
const [modalEl, setModalEl] = useState<HTMLDivElement | null>(null);
|
||||||
const modalRef = useCallback((el: HTMLDivElement | null) => setModalEl(el), []);
|
const modalRef = useCallback((el: HTMLDivElement | null) => setModalEl(el), []);
|
||||||
|
const releaseNotesOpen = useAtomValue(releaseNotesDialogAtom).open;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
|
active={!releaseNotesOpen}
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type PageRootProps = {
|
|||||||
export function PageRoot({ nav, children }: PageRootProps) {
|
export function PageRoot({ nav, children }: PageRootProps) {
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
const showCompactMaster = useShowCompactMasterView();
|
const showCompactMaster = useShowCompactMasterView();
|
||||||
|
const showDetail = !showCompactMaster || nav == null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
||||||
@@ -23,7 +24,7 @@ export function PageRoot({ nav, children }: PageRootProps) {
|
|||||||
{screenSize !== ScreenSize.Mobile && (
|
{screenSize !== ScreenSize.Mobile && (
|
||||||
<Line variant="Background" size="300" direction="Vertical" />
|
<Line variant="Background" size="300" direction="Vertical" />
|
||||||
)}
|
)}
|
||||||
{!showCompactMaster && children}
|
{showDetail && children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ 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,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 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))';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
Scroll,
|
Scroll,
|
||||||
Spinner,
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
|
usePopOutContainer,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { Icon, Icons } from '../icons';
|
import { Icon, Icons } from '../icons';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
@@ -35,6 +37,7 @@ export function UpdatesDialog({
|
|||||||
onClose,
|
onClose,
|
||||||
}: UpdatesDialogProps) {
|
}: UpdatesDialogProps) {
|
||||||
const [showOlderList, setShowOlderList] = useState(false);
|
const [showOlderList, setShowOlderList] = useState(false);
|
||||||
|
const popOutContainer = usePopOutContainer();
|
||||||
|
|
||||||
const displayVersion = activeDoc?.version;
|
const displayVersion = activeDoc?.version;
|
||||||
const displayTitle = activeDoc?.title ?? (displayVersion ? `Version ${displayVersion}` : 'Release notes');
|
const displayTitle = activeDoc?.title ?? (displayVersion ? `Version ${displayVersion}` : 'Release notes');
|
||||||
@@ -46,18 +49,23 @@ export function UpdatesDialog({
|
|||||||
setShowOlderList(false);
|
setShowOlderList(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const portalTarget =
|
||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
popOutContainer ??
|
||||||
<OverlayCenter className={css.OverlayFrame}>
|
document.getElementById('portalContainer') ??
|
||||||
<FocusTrap
|
document.body;
|
||||||
focusTrapOptions={{
|
|
||||||
initialFocus: false,
|
return createPortal(
|
||||||
onDeactivate: onClose,
|
<div className={css.PortalLayer}>
|
||||||
clickOutsideDeactivates: true,
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
escapeDeactivates: stopPropagation,
|
<OverlayCenter className={css.OverlayFrame}>
|
||||||
}}
|
<FocusTrap
|
||||||
>
|
focusTrapOptions={{
|
||||||
<Dialog variant="Surface" className={css.DialogShell}>
|
initialFocus: false,
|
||||||
|
clickOutsideDeactivates: false,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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 +185,7 @@ export function UpdatesDialog({
|
|||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
</OverlayCenter>
|
</OverlayCenter>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
</div>,
|
||||||
|
portalTarget
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ type UpdaterInfo = {
|
|||||||
export function useOpenReleaseNotesDialog(): () => void {
|
export function useOpenReleaseNotesDialog(): () => void {
|
||||||
const setDialogState = useSetAtom(releaseNotesDialogAtom);
|
const setDialogState = useSetAtom(releaseNotesDialogAtom);
|
||||||
return useCallback(() => {
|
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]);
|
}, [setDialogState]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +44,11 @@ export function UpdatesDialogHost() {
|
|||||||
setManifest(null);
|
setManifest(null);
|
||||||
setActiveDoc(null);
|
setActiveDoc(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
getAppVersion().then((version) => {
|
||||||
|
if (version !== 'unknown') {
|
||||||
|
setLastSeenAppVersion(version);
|
||||||
|
}
|
||||||
|
});
|
||||||
}, [setDialogState]);
|
}, [setDialogState]);
|
||||||
|
|
||||||
const selectFile = useCallback(async (file: string) => {
|
const selectFile = useCallback(async (file: string) => {
|
||||||
@@ -73,7 +81,7 @@ export function UpdatesDialogHost() {
|
|||||||
if (cancelled || currentVersion === 'unknown') return;
|
if (cancelled || currentVersion === 'unknown') return;
|
||||||
|
|
||||||
const lastSeen = getLastSeenAppVersion();
|
const lastSeen = getLastSeenAppVersion();
|
||||||
const shouldAutoShow = Boolean(lastSeen && lastSeen !== currentVersion);
|
const shouldAutoShow = lastSeen !== currentVersion;
|
||||||
|
|
||||||
if (shouldAutoShow) {
|
if (shouldAutoShow) {
|
||||||
const loadedManifest = await loadUpdateManifest();
|
const loadedManifest = await loadUpdateManifest();
|
||||||
@@ -87,10 +95,6 @@ export function UpdatesDialogHost() {
|
|||||||
setDialogState({ open: true, manual: false });
|
setDialogState({ open: true, manual: false });
|
||||||
clearPendingReleaseNotes();
|
clearPendingReleaseNotes();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastSeen || lastSeen !== currentVersion) {
|
|
||||||
setLastSeenAppVersion(currentVersion);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const timer = window.setTimeout(checkVersion, 1500);
|
const timer = window.setTimeout(checkVersion, 1500);
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
|
import bundledManifest from '../../../public/update/manifest.json';
|
||||||
import { trimTrailingSlash } from '../utils/common';
|
import { trimTrailingSlash } from '../utils/common';
|
||||||
|
|
||||||
const UPDATE_BASE_PATH = `${trimTrailingSlash(import.meta.env.BASE_URL)}/update`;
|
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 =
|
||||||
|
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;
|
||||||
@@ -55,27 +71,50 @@ 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 {
|
export function resolveUpdateAssetUrl(src: string | undefined): string | undefined {
|
||||||
if (!src) return 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;
|
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) {
|
||||||
|
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;
|
try {
|
||||||
if (!data?.current || !Array.isArray(data.older)) return null;
|
return loadBundledManifest();
|
||||||
|
|
||||||
return {
|
|
||||||
current: data.current,
|
|
||||||
older: data.older.filter((entry) => entry?.file),
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -86,14 +125,16 @@ 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) {
|
||||||
|
const source = await response.text();
|
||||||
const source = await response.text();
|
return parseUpdateMarkdown(safeFile, source);
|
||||||
return parseUpdateMarkdown(safeFile, source);
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
// fall through to bundled copy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return loadBundledDocument(safeFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadCurrentUpdateDocument(): Promise<ParsedUpdateDoc | null> {
|
export async function loadCurrentUpdateDocument(): Promise<ParsedUpdateDoc | null> {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export function About({ requestClose }: AboutProps) {
|
|||||||
const [version, setVersion] = useState<string>('');
|
const [version, setVersion] = useState<string>('');
|
||||||
const [protocolStatus, setProtocolStatus] = useState<string>('Checking desktop protocol integration...');
|
const [protocolStatus, setProtocolStatus] = useState<string>('Checking desktop protocol integration...');
|
||||||
const [protocolBusy, setProtocolBusy] = useState<boolean>(false);
|
const [protocolBusy, setProtocolBusy] = useState<boolean>(false);
|
||||||
|
const [updatePreview, setUpdatePreview] = useState<{ title: string; description: string } | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const formatProtocolStatus = useCallback((data: {
|
const formatProtocolStatus = useCallback((data: {
|
||||||
scheme: string;
|
scheme: string;
|
||||||
@@ -101,8 +104,6 @@ export function About({ requestClose }: AboutProps) {
|
|||||||
getCurrentUpdatePreview().then(setUpdatePreview).catch(() => setUpdatePreview(null));
|
getCurrentUpdatePreview().then(setUpdatePreview).catch(() => setUpdatePreview(null));
|
||||||
}, [refreshProtocolStatus]);
|
}, [refreshProtocolStatus]);
|
||||||
|
|
||||||
const [updatePreview, setUpdatePreview] = useState<{ title: string; description: string } | null>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader outlined={false}>
|
<PageHeader outlined={false}>
|
||||||
|
|||||||
@@ -699,10 +699,10 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
|
|||||||
<MessageNotifications />
|
<MessageNotifications />
|
||||||
<BackgroundSyncSetup />
|
<BackgroundSyncSetup />
|
||||||
<PaarrotAPIInitializer />
|
<PaarrotAPIInitializer />
|
||||||
<UpdatesDialogHost />
|
|
||||||
<TaskbarFlashStopper />
|
<TaskbarFlashStopper />
|
||||||
<AndroidShareIntentHandler />
|
<AndroidShareIntentHandler />
|
||||||
{children}
|
{children}
|
||||||
|
<UpdatesDialogHost />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export async function loadColorPreference(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractMemberColorPreference(room: Room | undefined, userId: string): ColorPreference | undefined {
|
export function extractMemberColorPreference(room: Room | undefined, userId: string): ColorPreference | undefined {
|
||||||
if (!room) return undefined;
|
if (!room || typeof (room as Room).getMember !== 'function' || !userId) return undefined;
|
||||||
const member = room.getMember(userId);
|
const member = room.getMember(userId);
|
||||||
const content = member?.events.member?.getContent();
|
const content = member?.events.member?.getContent();
|
||||||
if (!content) return undefined;
|
if (!content) return undefined;
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ const copyFiles = {
|
|||||||
{
|
{
|
||||||
src: 'public/update/**/*',
|
src: 'public/update/**/*',
|
||||||
dest: 'update',
|
dest: 'update',
|
||||||
rename: (_name, _ext, fullPath) => fullPath.replace(/^public[/\\]update[/\\]/, ''),
|
// stripBase removes public/update from the matched path; rename alone only changes the filename.
|
||||||
|
rename: { stripBase: 2 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user