From b08b4b41298786d3d279f76733f432c0548bad02 Mon Sep 17 00:00:00 2001 From: Litruv Date: Sun, 22 Feb 2026 00:59:14 +1100 Subject: [PATCH] feat: add update notification component with download and install functionality --- src/app/components/title-bar/TitleBar.tsx | 2 + .../UpdateNotification.css.ts | 76 ++++++ .../UpdateNotification.tsx | 243 ++++++++++++++++++ .../components/update-notification/index.ts | 1 + 4 files changed, 322 insertions(+) create mode 100644 src/app/components/update-notification/UpdateNotification.css.ts create mode 100644 src/app/components/update-notification/UpdateNotification.tsx create mode 100644 src/app/components/update-notification/index.ts diff --git a/src/app/components/title-bar/TitleBar.tsx b/src/app/components/title-bar/TitleBar.tsx index b762b55..6ef6667 100644 --- a/src/app/components/title-bar/TitleBar.tsx +++ b/src/app/components/title-bar/TitleBar.tsx @@ -5,6 +5,7 @@ import { MatrixClient, RoomEvent, ClientEvent, MatrixEvent, Room, IPushRules } f import { useAtomValue } from 'jotai'; import { useNavigate } from 'react-router-dom'; import { WindowControls } from './WindowControls'; +import { UpdateNotification } from '../update-notification'; import * as css from './TitleBar.css'; import { getUnreadInfos, getOrphanParents, guessPerfectParent, getAllParents } from '../../utils/room'; import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../utils/matrix'; @@ -615,6 +616,7 @@ export function TitleBar({ mx, children }: TitleBarProps) { ))} {children} + ); diff --git a/src/app/components/update-notification/UpdateNotification.css.ts b/src/app/components/update-notification/UpdateNotification.css.ts new file mode 100644 index 0000000..4bfb62e --- /dev/null +++ b/src/app/components/update-notification/UpdateNotification.css.ts @@ -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', +}); diff --git a/src/app/components/update-notification/UpdateNotification.tsx b/src/app/components/update-notification/UpdateNotification.tsx new file mode 100644 index 0000000..c790f63 --- /dev/null +++ b/src/app/components/update-notification/UpdateNotification.tsx @@ -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(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) => { + 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 ( + + + + ); + } + + // Show checking state + if (checking) { + return ( + + + + ); + } + + // Show update available/ready state + if (!updateAvailable && !updateReady) { + return null; + } + + return ( + + + + + + + + {updateReady ? 'Update Ready' : 'Update Available'} + + {updateInfo && ( + + Version {updateInfo.version} + + )} + + + {updateReady ? ( + + ) : ( + + )} + + + } + > + {null} + + + ); +} diff --git a/src/app/components/update-notification/index.ts b/src/app/components/update-notification/index.ts new file mode 100644 index 0000000..b8c25db --- /dev/null +++ b/src/app/components/update-notification/index.ts @@ -0,0 +1 @@ +export * from './UpdateNotification';