diff --git a/apps/desktop/src/lib/backend/updater.ts b/apps/desktop/src/lib/backend/updater.ts index c8feaeb7f..f45d83521 100644 --- a/apps/desktop/src/lib/backend/updater.ts +++ b/apps/desktop/src/lib/backend/updater.ts @@ -14,13 +14,21 @@ import posthog from 'posthog-js'; import { derived, writable, type Readable } from 'svelte/store'; // TODO: Investigate why 'DOWNLOADED' is not in the type provided by Tauri. -export type Update = - | { version?: string; status?: UpdateStatus | 'DOWNLOADED'; body?: string } - | undefined; +export type Update = { + version?: string; + status?: UpdateStatus | 'DOWNLOADED'; + body?: string; + manual: boolean; +}; export class UpdaterService { - private status = writable(undefined); - private result = writable(undefined, () => { + // True if manually initiated check. + private manual = writable(false); + + // An object rather than string to prevent unique deduplication. + private status = writable<{ status: UpdateStatus } | undefined>(undefined); + + private manifest = writable(undefined, () => { this.start(); return () => { this.stop(); @@ -28,41 +36,42 @@ export class UpdaterService { }); update: Readable = derived( - [this.status, this.result], - ([status, update]) => { - return { ...update, status }; + [this.status, this.manifest, this.manual], + ([status, result, manual]) => { + // Do not emit when up-to-date unless manually initiated. + if (status?.status === 'UPTODATE' && result && !manual) { + return; + } + return { ...result, ...status, manual }; }, undefined ); + // Needed to reset dismissed modal when version changes. currentVersion = writable(undefined); readonly version = derived(this.update, (update) => update?.version); - prev: Update | undefined; + intervalId: any; unlistenStatusFn: any; unlistenManualCheckFn: any; - intervalId: any; constructor() {} private async start() { - const currentVersion = await getVersion(); - this.currentVersion.set(currentVersion); + this.currentVersion.set(await getVersion()); this.unlistenManualCheckFn = listen('menu://global/update/clicked', () => { this.checkForUpdate(true); }); - - this.unlistenStatusFn = await onUpdaterEvent((status) => { - const err = status.error; - if (err) { - showErrorToast(err); - posthog.capture('App Update Status Error', { error: err }); + this.unlistenStatusFn = await onUpdaterEvent((event) => { + const { error, status } = event; + if (error) { + showErrorToast(error); + posthog.capture('App Update Status Error', { error }); } - this.status.set(status.status); + this.status.set({ status }); }); - - await this.checkForUpdate(); setInterval(async () => await this.checkForUpdate(), 3600000); // hourly + this.checkForUpdate(); } private async stop() { @@ -74,17 +83,18 @@ export class UpdaterService { } } - async checkForUpdate(isManual = false) { + async checkForUpdate(manual = false) { const update = await Promise.race([ checkUpdate(), // In DEV mode this never returns. new Promise((resolve) => setTimeout(() => resolve({ shouldUpdate: false }), 30000) ) ]); - if (!update.shouldUpdate && isManual) { - this.status.set('UPTODATE'); + this.manual.set(manual); + if (!update.shouldUpdate && manual) { + this.status.set({ status: 'UPTODATE' }); } else if (update.manifest) { - this.result.set(update.manifest); + this.manifest.set(update.manifest); } } diff --git a/apps/desktop/src/lib/components/AppUpdater.svelte b/apps/desktop/src/lib/components/AppUpdater.svelte index f91ce49fb..b25819363 100644 --- a/apps/desktop/src/lib/components/AppUpdater.svelte +++ b/apps/desktop/src/lib/components/AppUpdater.svelte @@ -2,50 +2,58 @@ import Link from '../shared/Link.svelte'; import newVersionSvg from '$lib/assets/empty-state/app-new-version.svg?raw'; import upToDateSvg from '$lib/assets/empty-state/app-up-to-date.svg?raw'; - import { UpdaterService } from '$lib/backend/updater'; + import { UpdaterService, type Update } from '$lib/backend/updater'; import { getContext } from '$lib/utils/context'; import Button from '@gitbutler/ui/inputs/Button.svelte'; import Modal from '@gitbutler/ui/modal/Modal.svelte'; const updaterService = getContext(UpdaterService); - const update = updaterService.update; const currentVersion = updaterService.currentVersion; - const status = $derived($update?.status); - const version = $derived($update?.version); + const update = updaterService.update; - let dismissed = $state(false); - let open = $state(false); - - let modalRef: Modal; + let status = $state(); + let version = $state(); + let lastVersion: string | undefined; + let dismissed = false; + let modal: Modal; $effect(() => { - if (version || (status && status !== 'ERROR' && !dismissed && !open)) { - open = true; + if ($update) { + console.log($update); + handleUpdate($update); } }); - $effect(() => { - if (status === 'ERROR') { - open = false; - } - }); + function handleUpdate(update: Update) { + version = update?.version; - $effect(() => { - if (open) { - modalRef.show(); - } else { - modalRef.close(); + if (version !== lastVersion) { + dismissed = false; } - }); + + status = update?.status; + const manual = update?.manual; + + if (manual) { + modal.show(); + } else if (status === 'ERROR') { + modal.close(); + } else if (status && status !== 'UPTODATE' && !dismissed) { + modal.show(); + } else if (version && !dismissed) { + modal.show(); + } + lastVersion = version; + } function handleDismiss() { dismissed = true; - open = false; + modal.close(); } - +