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';