feat: add update notification component with download and install functionality
This commit is contained in:
@@ -5,6 +5,7 @@ import { MatrixClient, RoomEvent, ClientEvent, MatrixEvent, Room, IPushRules } f
|
|||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { WindowControls } from './WindowControls';
|
import { WindowControls } from './WindowControls';
|
||||||
|
import { UpdateNotification } from '../update-notification';
|
||||||
import * as css from './TitleBar.css';
|
import * as css from './TitleBar.css';
|
||||||
import { getUnreadInfos, getOrphanParents, guessPerfectParent, getAllParents } from '../../utils/room';
|
import { getUnreadInfos, getOrphanParents, guessPerfectParent, getAllParents } from '../../utils/room';
|
||||||
import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../utils/matrix';
|
import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../utils/matrix';
|
||||||
@@ -615,6 +616,7 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
|||||||
))}
|
))}
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
<UpdateNotification />
|
||||||
<WindowControls />
|
<WindowControls />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { color, config, toRem } from 'folds';
|
||||||
|
|
||||||
|
export const CheckButton = style({
|
||||||
|
all: 'unset',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: config.space.S200,
|
||||||
|
borderRadius: config.radii.R300,
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: color.Secondary.Main,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
transition: 'background-color 0.15s',
|
||||||
|
minWidth: toRem(32),
|
||||||
|
minHeight: toRem(32),
|
||||||
|
opacity: 0.7,
|
||||||
|
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: color.Surface.ContainerHover,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
':active': {
|
||||||
|
backgroundColor: color.Surface.ContainerActive,
|
||||||
|
},
|
||||||
|
|
||||||
|
':focus-visible': {
|
||||||
|
outline: `2px solid ${color.Secondary.Main}`,
|
||||||
|
outlineOffset: '2px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateButton = style({
|
||||||
|
all: 'unset',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: config.space.S200,
|
||||||
|
borderRadius: config.radii.R300,
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: color.Success.Main,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
transition: 'background-color 0.15s',
|
||||||
|
minWidth: toRem(32),
|
||||||
|
minHeight: toRem(32),
|
||||||
|
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: color.Surface.ContainerHover,
|
||||||
|
},
|
||||||
|
|
||||||
|
':active': {
|
||||||
|
backgroundColor: color.Surface.ContainerActive,
|
||||||
|
},
|
||||||
|
|
||||||
|
':focus-visible': {
|
||||||
|
outline: `2px solid ${color.Success.Main}`,
|
||||||
|
outlineOffset: '2px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateMenu = style({
|
||||||
|
minWidth: toRem(280),
|
||||||
|
maxWidth: toRem(320),
|
||||||
|
backgroundColor: color.Surface.Container,
|
||||||
|
borderRadius: config.radii.R400,
|
||||||
|
boxShadow: config.shadow.E400,
|
||||||
|
border: `1px solid ${color.Surface.ContainerLine}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ProgressText = style({
|
||||||
|
color: color.Success.Main,
|
||||||
|
fontWeight: 500,
|
||||||
|
minWidth: toRem(35),
|
||||||
|
textAlign: 'center',
|
||||||
|
});
|
||||||
243
src/app/components/update-notification/UpdateNotification.tsx
Normal file
243
src/app/components/update-notification/UpdateNotification.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Box, IconButton, Spinner, Text, Menu, PopOut, Button, config } from 'folds';
|
||||||
|
import * as css from './UpdateNotification.css';
|
||||||
|
|
||||||
|
interface UpdateInfo {
|
||||||
|
version: string;
|
||||||
|
releaseNotes?: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadProgress {
|
||||||
|
percent: number;
|
||||||
|
transferred: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpdateNotification() {
|
||||||
|
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||||
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||||
|
const [downloading, setDownloading] = useState(false);
|
||||||
|
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||||
|
const [updateReady, setUpdateReady] = useState(false);
|
||||||
|
const [checking, setChecking] = useState(false);
|
||||||
|
const [menuAnchor, setMenuAnchor] = useState<{ x: number; y: number; width: number; height: number } | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if we're in Electron environment
|
||||||
|
if (typeof window === 'undefined' || !(window as any).electron?.updater) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updater } = (window as any).electron;
|
||||||
|
|
||||||
|
// Listen for update available
|
||||||
|
updater.onUpdateAvailable((info: UpdateInfo) => {
|
||||||
|
console.log('Update available:', info.version);
|
||||||
|
setUpdateAvailable(true);
|
||||||
|
setUpdateInfo(info);
|
||||||
|
setDownloading(false);
|
||||||
|
setUpdateReady(false);
|
||||||
|
setChecking(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for download progress
|
||||||
|
updater.onUpdateDownloadProgress((progress: DownloadProgress) => {
|
||||||
|
setDownloadProgress(Math.round(progress.percent));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for update downloaded
|
||||||
|
updater.onUpdateDownloaded((info: UpdateInfo) => {
|
||||||
|
console.log('Update downloaded:', info.version);
|
||||||
|
setDownloading(false);
|
||||||
|
setUpdateReady(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup - IPC listeners don't need manual cleanup in this case
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCheckForUpdates = async () => {
|
||||||
|
setChecking(true);
|
||||||
|
try {
|
||||||
|
const result = await (window as any).electron.updater.checkForUpdates();
|
||||||
|
console.log('Update check result:', result);
|
||||||
|
// If no update found, show feedback
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!updateAvailable) {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check for updates:', error);
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
setDownloading(true);
|
||||||
|
setDownloadProgress(0);
|
||||||
|
try {
|
||||||
|
await (window as any).electron.updater.downloadUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download update:', error);
|
||||||
|
setDownloading(false);
|
||||||
|
}
|
||||||
|
setMenuAnchor(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstall = async () => {
|
||||||
|
try {
|
||||||
|
await (window as any).electron.updater.installUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to install update:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (menuAnchor) {
|
||||||
|
setMenuAnchor(undefined);
|
||||||
|
} else {
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
|
setMenuAnchor({ x: rect.x, y: rect.y, width: rect.width, height: rect.height });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't render anything if not in Electron
|
||||||
|
if (typeof window === 'undefined' || !(window as any).electron?.updater) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show check button if no update status
|
||||||
|
if (!updateAvailable && !updateReady && !checking) {
|
||||||
|
return (
|
||||||
|
<Box gap="100" alignItems="Center">
|
||||||
|
<button
|
||||||
|
className={css.CheckButton}
|
||||||
|
onClick={handleCheckForUpdates}
|
||||||
|
aria-label="Check for updates"
|
||||||
|
title="Check for updates"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path
|
||||||
|
d="M8 2V10M8 10L5 7M8 10L11 7"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 14H13"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show checking state
|
||||||
|
if (checking) {
|
||||||
|
return (
|
||||||
|
<Box gap="100" alignItems="Center">
|
||||||
|
<button className={css.UpdateButton} disabled type="button">
|
||||||
|
<Spinner variant="Secondary" size="50" />
|
||||||
|
</button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show update available/ready state
|
||||||
|
if (!updateAvailable && !updateReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box gap="100" alignItems="Center">
|
||||||
|
<button
|
||||||
|
className={css.UpdateButton}
|
||||||
|
onClick={handleMenuToggle}
|
||||||
|
aria-label={updateReady ? 'Update ready' : 'Update available'}
|
||||||
|
title={updateReady ? 'Update downloaded - click to install' : 'New version available'}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{downloading ? (
|
||||||
|
<Box alignItems="Center" gap="100">
|
||||||
|
<Spinner variant="Secondary" size="50" />
|
||||||
|
{downloadProgress > 0 && (
|
||||||
|
<Text size="T200" className={css.ProgressText}>
|
||||||
|
{downloadProgress}%
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path
|
||||||
|
d="M8 2V10M8 10L5 7M8 10L11 7"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 14H13"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<PopOut
|
||||||
|
anchor={menuAnchor}
|
||||||
|
position="Bottom"
|
||||||
|
align="End"
|
||||||
|
offset={8}
|
||||||
|
content={
|
||||||
|
<Menu className={css.UpdateMenu}>
|
||||||
|
<Box direction="Column" gap="200" style={{ padding: config.space.S300 }}>
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<Text size="H5" priority="400">
|
||||||
|
{updateReady ? 'Update Ready' : 'Update Available'}
|
||||||
|
</Text>
|
||||||
|
{updateInfo && (
|
||||||
|
<Text size="T200" priority="300">
|
||||||
|
Version {updateInfo.version}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{updateReady ? (
|
||||||
|
<Button
|
||||||
|
variant="Primary"
|
||||||
|
size="400"
|
||||||
|
onClick={handleInstall}
|
||||||
|
fill="Solid"
|
||||||
|
>
|
||||||
|
<Text size="B400">Install and Restart</Text>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="Primary"
|
||||||
|
size="400"
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={downloading}
|
||||||
|
fill="Solid"
|
||||||
|
>
|
||||||
|
<Text size="B400">
|
||||||
|
{downloading ? `Downloading ${downloadProgress}%` : 'Download Update'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{null}
|
||||||
|
</PopOut>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/app/components/update-notification/index.ts
Normal file
1
src/app/components/update-notification/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './UpdateNotification';
|
||||||
Reference in New Issue
Block a user