diff --git a/fluxer_app/src/features/app/state/Updater.ts b/fluxer_app/src/features/app/state/Updater.ts index adb3c3d70..88d8bb50d 100644 --- a/fluxer_app/src/features/app/state/Updater.ts +++ b/fluxer_app/src/features/app/state/Updater.ts @@ -155,6 +155,7 @@ class Updater { private unsubscribeNativeEvents: (() => void) | null = null; private unsubscribeDesktopUpdate: (() => void) | null = null; private desktopUpdateReported = false; + private desktopUpdateReportedReplacesShell = false; private desktopUpdateRunning = false; private olderShellModuleUpdateReady = false; private olderShellUpdateDownloaded = false; @@ -203,6 +204,10 @@ class Updater { return this.desktopUpdateReported || this.olderShellModuleUpdateReady || this.olderShellUpdateAvailable; } + get desktopUpdateReplacesShell(): boolean { + return (this.desktopUpdateReported && this.desktopUpdateReportedReplacesShell) || this.olderShellUpdateAvailable; + } + private get olderShellUpdateAvailable(): boolean { const electronApi = getElectronAPI(); if (electronApi == null || electronApi.desktopUpdate != null || electronApi.updaterInstall == null) { @@ -281,6 +286,7 @@ class Updater { this.unsubscribeDesktopUpdate = desktopUpdate.onStateChanged((state) => { runInAction(() => { this.desktopUpdateReported = state.available; + this.desktopUpdateReportedReplacesShell = state.replacesShell === true; this.desktopUpdateRunning = state.updating === true; }); }); @@ -314,6 +320,7 @@ class Updater { const state = await desktopUpdate.state(); runInAction(() => { this.desktopUpdateReported = state.available; + this.desktopUpdateReportedReplacesShell = state.replacesShell === true; this.desktopUpdateRunning = state.updating === true; }); } catch (error) { @@ -669,6 +676,7 @@ class Updater { this.unsubscribeDesktopUpdate = null; } this.desktopUpdateReported = false; + this.desktopUpdateReportedReplacesShell = false; this.desktopUpdateRunning = false; this.olderShellModuleUpdateReady = false; this.olderShellUpdateDownloaded = false; diff --git a/fluxer_app/src/features/channel/components/channel_header_components/UpdaterIcon.tsx b/fluxer_app/src/features/channel/components/channel_header_components/UpdaterIcon.tsx index ef9477c90..fc024e91f 100644 --- a/fluxer_app/src/features/channel/components/channel_header_components/UpdaterIcon.tsx +++ b/fluxer_app/src/features/channel/components/channel_header_components/UpdaterIcon.tsx @@ -8,7 +8,7 @@ import FocusRing from '@app/features/ui/focus_ring/FocusRing'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; -import {ArrowClockwiseIcon} from '@phosphor-icons/react'; +import {ArrowClockwiseIcon, DownloadSimpleIcon} from '@phosphor-icons/react'; import {observer} from 'mobx-react-lite'; import {useCallback} from 'react'; @@ -67,6 +67,10 @@ export const UpdaterIcon = observer(() => { ? i18n._(CLICK_TO_RELOAD_AND_UPDATE_DESCRIPTOR, {version, productName: RuntimeConfig.productName}) : i18n._(CLICK_TO_RELOAD_AND_UPDATE_2_DESCRIPTOR, {productName: RuntimeConfig.productName}); } + const Icon = + affordance === 'manual' || (affordance === 'desktop' && Updater.desktopUpdateReplacesShell) + ? DownloadSimpleIcon + : ArrowClockwiseIcon; return ( @@ -79,7 +83,7 @@ export const UpdaterIcon = observer(() => { aria-label={tooltip} data-flx="channel.channel-header-components.updater-icon.button.click" > - { test('nothing is available and nothing starts before the bootstrap arms the gate', async () => { - assert.deepEqual(getDesktopUpdateState(), {available: false, updating: false}); - assert.deepEqual(await checkDesktopUpdateNow(), {available: false, updating: false}); + assert.deepEqual(getDesktopUpdateState(), {available: false, updating: false, replacesShell: false}); + assert.deepEqual(await checkDesktopUpdateNow(), {available: false, updating: false, replacesShell: false}); assert.equal(startDesktopUpdate(), false); }); @@ -44,17 +44,17 @@ describe('DesktopUpdateGate', () => { }, }); - assert.deepEqual(await checkDesktopUpdateNow(), {available: true, updating: false}); + assert.deepEqual(await checkDesktopUpdateNow(), {available: true, updating: false, replacesShell: false}); assert.equal(startDesktopUpdate(), true); assert.equal(startDesktopUpdate(), false, 'a second click while the update runs is a no-op'); assert.deepEqual( getDesktopUpdateState(), - {available: false, updating: true}, + {available: false, updating: true, replacesShell: false}, 'the affordance hides the moment it is clicked', ); assert.deepEqual( await checkDesktopUpdateNow(), - {available: false, updating: true}, + {available: false, updating: true, replacesShell: false}, 'a poll during the update changes nothing', ); @@ -62,11 +62,11 @@ describe('DesktopUpdateGate', () => { await new Promise((resolve) => setImmediate(resolve)); assert.equal(starts, 1); - assert.deepEqual(getDesktopUpdateState(), {available: false, updating: false}); + assert.deepEqual(getDesktopUpdateState(), {available: false, updating: false, replacesShell: false}); assert.deepEqual(states, [ - {available: true, updating: false}, - {available: false, updating: true}, - {available: false, updating: false}, + {available: true, updating: false, replacesShell: false}, + {available: false, updating: true, replacesShell: false}, + {available: false, updating: false, replacesShell: false}, ]); stop(); }); @@ -111,7 +111,7 @@ describe('DesktopUpdateGate', () => { pending.resolve(); await background; - assert.deepEqual(await manual, {available: true, updating: false}); + assert.deepEqual(await manual, {available: true, updating: false, replacesShell: true}); assert.deepEqual(calls, [false, true]); }); @@ -127,7 +127,7 @@ describe('DesktopUpdateGate', () => { assert.equal(startDesktopUpdate(), true); await new Promise((resolve) => setImmediate(resolve)); - assert.deepEqual(getDesktopUpdateState(), {available: true, updating: false}); + assert.deepEqual(getDesktopUpdateState(), {available: true, updating: false, replacesShell: true}); assert.equal(startDesktopUpdate(), true); }); }); diff --git a/fluxer_desktop/src/main/DesktopUpdateGate.ts b/fluxer_desktop/src/main/DesktopUpdateGate.ts index 716c8dcb9..f7ae693f5 100644 --- a/fluxer_desktop/src/main/DesktopUpdateGate.ts +++ b/fluxer_desktop/src/main/DesktopUpdateGate.ts @@ -24,6 +24,7 @@ interface DesktopUpdateGateState { checking: {readonly manual: boolean; readonly settled: Promise} | null; notifiedAvailable: boolean; notifiedUpdating: boolean; + notifiedReplacesShell: boolean; readonly listeners: Set; } @@ -34,6 +35,7 @@ const gateState: DesktopUpdateGateState = { checking: null, notifiedAvailable: false, notifiedUpdating: false, + notifiedReplacesShell: false, listeners: new Set(), }; @@ -42,9 +44,11 @@ function isAvailable(check: DesktopUpdateCheck | null): boolean { } export function getDesktopUpdateState(): DesktopUpdateState { + const available = gateState.running == null && isAvailable(gateState.lastCheck); return { - available: gateState.running == null && isAvailable(gateState.lastCheck), + available, updating: gateState.running != null, + replacesShell: available && gateState.lastCheck?.shellNewer === true, }; } @@ -55,11 +59,17 @@ export function desktopUpdateReplacesShell(): boolean { function notify(): void { const state = getDesktopUpdateState(); const updating = state.updating === true; - if (state.available === gateState.notifiedAvailable && updating === gateState.notifiedUpdating) { + const replacesShell = state.replacesShell === true; + if ( + state.available === gateState.notifiedAvailable && + updating === gateState.notifiedUpdating && + replacesShell === gateState.notifiedReplacesShell + ) { return; } gateState.notifiedAvailable = state.available; gateState.notifiedUpdating = updating; + gateState.notifiedReplacesShell = replacesShell; for (const listener of Array.from(gateState.listeners)) { try { listener(state); diff --git a/packages/desktop_ipc/src/ModuleContract.ts b/packages/desktop_ipc/src/ModuleContract.ts index 802087194..268580c06 100644 --- a/packages/desktop_ipc/src/ModuleContract.ts +++ b/packages/desktop_ipc/src/ModuleContract.ts @@ -57,6 +57,7 @@ export interface DesktopModuleAPI { export interface DesktopUpdateState { readonly available: boolean; readonly updating?: boolean; + readonly replacesShell?: boolean; } export interface DesktopUpdateAPI {