From 2eecaddee15f430293c68a7d3cba9f93f7b08447 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 8 Dec 2025 01:44:12 +0200 Subject: [PATCH] feat: enhance update dialog with changelog notes --- packages/desktop/src-tauri/Cargo.lock | 2 +- packages/desktop/src/api/updater.ts | 48 +++++++++++++-- .../ui/src/components/ui/UpdateDialog.tsx | 46 +++++++++----- packages/ui/src/hooks/useUpdateCheck.ts | 61 +++++++++++++------ packages/ui/src/lib/desktop.ts | 1 + 5 files changed, 118 insertions(+), 40 deletions(-) diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index be8242be..ab002fd5 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2847,7 +2847,7 @@ dependencies = [ [[package]] name = "openchamber-desktop" -version = "1.0.7" +version = "1.0.8" dependencies = [ "anyhow", "axum", diff --git a/packages/desktop/src/api/updater.ts b/packages/desktop/src/api/updater.ts index 1dc99bdc..efe2a0d1 100644 --- a/packages/desktop/src/api/updater.ts +++ b/packages/desktop/src/api/updater.ts @@ -30,21 +30,26 @@ let cachedUpdate: Update | null = null; export async function checkForUpdates(): Promise { try { const { check } = await import('@tauri-apps/plugin-updater'); - const update = await check(); + const [update, currentVersion] = await Promise.all([ + check(), + getCurrentVersion(), + ]); cachedUpdate = update; if (!update) { return { available: false, - currentVersion: await getCurrentVersion(), + currentVersion, }; } + const changelogNotes = await fetchChangelogNotes(currentVersion, update.version); + return { available: true, version: update.version, - currentVersion: await getCurrentVersion(), - body: update.body ?? undefined, + currentVersion, + body: changelogNotes ?? update.body ?? undefined, date: update.date ?? undefined, }; } catch (error) { @@ -103,3 +108,38 @@ async function getCurrentVersion(): Promise { return 'unknown'; } } + +async function fetchChangelogNotes(fromVersion: string, toVersion: string): Promise { + try { + const response = await fetch( + 'https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md' + ); + if (!response.ok) return undefined; + + const changelog = await response.text(); + const sections = changelog.split(/^## /m).slice(1); + + const fromNum = parseVersion(fromVersion); + const toNum = parseVersion(toVersion); + + const relevantSections = sections.filter((section) => { + const match = section.match(/^\[(\d+\.\d+\.\d+)\]/); + if (!match) return false; + const ver = parseVersion(match[1]); + return ver > fromNum && ver <= toNum; + }); + + if (relevantSections.length === 0) return undefined; + + return relevantSections + .map((s) => '## ' + s.trim()) + .join('\n\n'); + } catch { + return undefined; + } +} + +function parseVersion(version: string): number { + const parts = version.split('.').map(Number); + return (parts[0] || 0) * 10000 + (parts[1] || 0) * 100 + (parts[2] || 0); +} diff --git a/packages/ui/src/components/ui/UpdateDialog.tsx b/packages/ui/src/components/ui/UpdateDialog.tsx index ac8d33de..23cc01ad 100644 --- a/packages/ui/src/components/ui/UpdateDialog.tsx +++ b/packages/ui/src/components/ui/UpdateDialog.tsx @@ -5,7 +5,8 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { RiDownloadLine, RiExternalLinkLine, RiLoaderLine, RiRestartLine, RiSparklingLine } from '@remixicon/react'; +import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; +import { RiDownloadCloudLine, RiDownloadLine, RiExternalLinkLine, RiLoaderLine, RiRestartLine } from '@remixicon/react'; import { cn } from '@/lib/utils'; import type { UpdateInfo, UpdateProgress } from '@/lib/desktop'; @@ -47,24 +48,38 @@ export const UpdateDialog: React.FC = ({ - + Update Available
- {info?.currentVersion && ( -
- Current version - {info.currentVersion} + {(info?.currentVersion || info?.version) && ( +
+ {info?.currentVersion && ( + {info.currentVersion} + )} + {info?.currentVersion && info?.version && ( + + )} + {info?.version && ( + {info.version} + )}
)} - {info?.version && ( -
- New version - {info.version} -
+ {info?.body && ( + +
+ {info.body + .replace(/^## \[(\d+\.\d+\.\d+)\] - \d{4}-\d{2}-\d{2}\s*/gm, '— v$1 —\n') + .replace(/^- /gm, '• ') + .trim()} +
+
)} {downloading && ( @@ -94,15 +109,14 @@ export const UpdateDialog: React.FC = ({ target="_blank" rel="noopener noreferrer" className={cn( - 'flex-1 flex items-center justify-center gap-2 px-3 py-1.5 rounded-md', - 'text-sm font-medium', - 'border border-border', - 'hover:bg-accent hover:text-accent-foreground', + 'flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md', + 'text-sm text-muted-foreground', + 'hover:text-foreground hover:bg-accent', 'transition-colors' )} > - View Release Notes + GitHub {!downloaded && !downloading && ( diff --git a/packages/ui/src/hooks/useUpdateCheck.ts b/packages/ui/src/hooks/useUpdateCheck.ts index 915d6f93..c1f07180 100644 --- a/packages/ui/src/hooks/useUpdateCheck.ts +++ b/packages/ui/src/hooks/useUpdateCheck.ts @@ -25,31 +25,43 @@ export type UseUpdateCheckReturn = UpdateState & { dismiss: () => void; }; -const MOCK_UPDATE: UpdateState = { +interface MockUpdateConfig { + currentVersion?: string; + newVersion?: string; +} + +// Set window.__OPENCHAMBER_MOCK_UPDATE__ = { currentVersion: '1.0.3', newVersion: '1.0.8' } to test with real changelog +declare global { + interface Window { + __OPENCHAMBER_MOCK_UPDATE__?: boolean | MockUpdateConfig; + } +} + +const getMockConfig = (): MockUpdateConfig | null => { + if (typeof window === 'undefined') return null; + const mock = window.__OPENCHAMBER_MOCK_UPDATE__; + if (!mock) return null; + if (mock === true) return { currentVersion: '1.0.3', newVersion: '1.0.8' }; + return mock; +}; + +const createMockUpdate = (config: MockUpdateConfig): UpdateState => ({ checking: false, available: true, downloading: false, downloaded: false, info: { available: true, - version: '99.0.0-test', - currentVersion: '0.0.0', - body: 'Test update for UI development', + version: config.newVersion ?? '99.0.0-test', + currentVersion: config.currentVersion ?? '0.0.0', + body: undefined, }, progress: null, error: null, -}; - -// Set window.__OPENCHAMBER_MOCK_UPDATE__ = true in console to test UI -declare global { - interface Window { - __OPENCHAMBER_MOCK_UPDATE__?: boolean; - } -} +}); const shouldMockUpdate = (): boolean => { - if (typeof window === 'undefined') return false; - return window.__OPENCHAMBER_MOCK_UPDATE__ === true; + return getMockConfig() !== null; }; export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => { @@ -64,7 +76,7 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => { }); const [mockMode, setMockMode] = useState(shouldMockUpdate); - const [mockState, setMockState] = useState(MOCK_UPDATE); + const [mockState, setMockState] = useState(null); // Check for mock mode changes (for console toggling) useEffect(() => { @@ -73,7 +85,10 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => { if (shouldMock !== mockMode) { setMockMode(shouldMock); if (shouldMock) { - setMockState(MOCK_UPDATE); + const config = getMockConfig(); + if (config) { + setMockState(createMockUpdate(config)); + } } } }, 500); @@ -82,7 +97,15 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => { const checkForUpdates = useCallback(async () => { if (mockMode) { - setMockState(MOCK_UPDATE); + const config = getMockConfig(); + if (config) { + const mockUpdate = createMockUpdate(config); + mockUpdate.info = { + ...mockUpdate.info!, + body: `## [${config.newVersion}] - 2025-01-01\n- Mock update for UI testing`, + }; + setMockState(mockUpdate); + } return; } if (!isDesktopRuntime()) { @@ -173,7 +196,7 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => { const dismiss = useCallback(() => { if (mockMode) { - setMockState(MOCK_UPDATE); + setMockState(null); return; } setState((prev) => ({ ...prev, available: false, downloaded: false, info: null })); @@ -189,7 +212,7 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => { } }, [checkOnMount, checkForUpdates, mockMode]); - const currentState = mockMode ? mockState : state; + const currentState = (mockMode && mockState) ? mockState : state; return { ...currentState, diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index 8a372baa..883c41a9 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -274,3 +274,4 @@ export const restartToApplyUpdate = async (): Promise => { return false; } }; +