Compare commits
2 Commits
b6f3f5c0aa
...
7d866404b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d866404b3 | |||
| c264de53b5 |
@@ -96,6 +96,12 @@
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<div id="portalContainer"></div>
|
||||
<style>
|
||||
#portalContainer {
|
||||
position: relative;
|
||||
z-index: 10000;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { ReactNode, useCallback, useState } from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Modal, Overlay, OverlayBackdrop, OverlayCenter, PopOutContainerProvider } from 'folds';
|
||||
import { stopPropagation } from '../utils/keyboard';
|
||||
import { releaseNotesDialogAtom } from '../state/releaseNotes';
|
||||
|
||||
type Modal500Props = {
|
||||
requestClose: () => void;
|
||||
@@ -10,11 +12,13 @@ type Modal500Props = {
|
||||
export function Modal500({ requestClose, children }: Modal500Props) {
|
||||
const [modalEl, setModalEl] = useState<HTMLDivElement | null>(null);
|
||||
const modalRef = useCallback((el: HTMLDivElement | null) => setModalEl(el), []);
|
||||
const releaseNotesOpen = useAtomValue(releaseNotesDialogAtom).open;
|
||||
|
||||
return (
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
active={!releaseNotesOpen}
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
clickOutsideDeactivates: true,
|
||||
|
||||
@@ -16,6 +16,7 @@ type PageRootProps = {
|
||||
export function PageRoot({ nav, children }: PageRootProps) {
|
||||
const screenSize = useScreenSizeContext();
|
||||
const showCompactMaster = useShowCompactMasterView();
|
||||
const showDetail = !showCompactMaster || nav == null;
|
||||
|
||||
return (
|
||||
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
||||
@@ -23,7 +24,7 @@ export function PageRoot({ nav, children }: PageRootProps) {
|
||||
{screenSize !== ScreenSize.Mobile && (
|
||||
<Line variant="Background" size="300" direction="Vertical" />
|
||||
)}
|
||||
{!showCompactMaster && children}
|
||||
{showDetail && children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -4,7 +4,6 @@ import FocusTrap from 'focus-trap-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
config,
|
||||
Dialog,
|
||||
IconButton,
|
||||
Overlay,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
Scroll,
|
||||
Spinner,
|
||||
Text,
|
||||
usePopOutContainer,
|
||||
} from 'folds';
|
||||
import { Icon, Icons } from '../icons';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
@@ -37,6 +37,7 @@ export function UpdatesDialog({
|
||||
onClose,
|
||||
}: UpdatesDialogProps) {
|
||||
const [showOlderList, setShowOlderList] = useState(false);
|
||||
const popOutContainer = usePopOutContainer();
|
||||
|
||||
const displayVersion = activeDoc?.version;
|
||||
const displayTitle = activeDoc?.title ?? (displayVersion ? `Version ${displayVersion}` : 'Release notes');
|
||||
@@ -48,6 +49,11 @@ export function UpdatesDialog({
|
||||
setShowOlderList(false);
|
||||
};
|
||||
|
||||
const portalTarget =
|
||||
popOutContainer ??
|
||||
document.getElementById('portalContainer') ??
|
||||
document.body;
|
||||
|
||||
return createPortal(
|
||||
<div className={css.PortalLayer}>
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
@@ -55,8 +61,7 @@ export function UpdatesDialog({
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: onClose,
|
||||
clickOutsideDeactivates: true,
|
||||
clickOutsideDeactivates: false,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
@@ -181,6 +186,6 @@ export function UpdatesDialog({
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
</div>,
|
||||
document.body
|
||||
portalTarget
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
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<ParsedUpdateDoc
|
||||
|
||||
try {
|
||||
const response = await fetch(getUpdateFileUrl(safeFile), { cache: 'no-cache' });
|
||||
if (!response.ok) return null;
|
||||
|
||||
const source = await response.text();
|
||||
return parseUpdateMarkdown(safeFile, source);
|
||||
if (response.ok) {
|
||||
const source = await response.text();
|
||||
return parseUpdateMarkdown(safeFile, source);
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
// fall through to bundled copy
|
||||
}
|
||||
|
||||
return loadBundledDocument(safeFile);
|
||||
}
|
||||
|
||||
export async function loadCurrentUpdateDocument(): Promise<ParsedUpdateDoc | null> {
|
||||
|
||||
@@ -21,6 +21,9 @@ export function About({ requestClose }: AboutProps) {
|
||||
const [version, setVersion] = useState<string>('');
|
||||
const [protocolStatus, setProtocolStatus] = useState<string>('Checking desktop protocol integration...');
|
||||
const [protocolBusy, setProtocolBusy] = useState<boolean>(false);
|
||||
const [updatePreview, setUpdatePreview] = useState<{ title: string; description: string } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const formatProtocolStatus = useCallback((data: {
|
||||
scheme: string;
|
||||
@@ -101,8 +104,6 @@ export function About({ requestClose }: AboutProps) {
|
||||
getCurrentUpdatePreview().then(setUpdatePreview).catch(() => setUpdatePreview(null));
|
||||
}, [refreshProtocolStatus]);
|
||||
|
||||
const [updatePreview, setUpdatePreview] = useState<{ title: string; description: string } | null>(null);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader outlined={false}>
|
||||
|
||||
@@ -152,7 +152,7 @@ export async function loadColorPreference(
|
||||
}
|
||||
|
||||
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 content = member?.events.member?.getContent();
|
||||
if (!content) return undefined;
|
||||
|
||||
@@ -56,7 +56,8 @@ const copyFiles = {
|
||||
{
|
||||
src: 'public/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