fix(app): show download icon for shell updates (#3471)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-14 17:48:41 +02:00
1 parent 5f5e26a8d4
commit 7c16d424ce
5 files changed
+38 -15

No files matched your search

@@ -155,6 +155,7 @@ class Updater {
private unsubscribeNativeEvents: (() => void) | null = null; private unsubscribeNativeEvents: (() => void) | null = null;
private unsubscribeDesktopUpdate: (() => void) | null = null; private unsubscribeDesktopUpdate: (() => void) | null = null;
private desktopUpdateReported = false; private desktopUpdateReported = false;
private desktopUpdateReportedReplacesShell = false;
private desktopUpdateRunning = false; private desktopUpdateRunning = false;
private olderShellModuleUpdateReady = false; private olderShellModuleUpdateReady = false;
private olderShellUpdateDownloaded = false; private olderShellUpdateDownloaded = false;
@@ -203,6 +204,10 @@ class Updater {
return this.desktopUpdateReported || this.olderShellModuleUpdateReady || this.olderShellUpdateAvailable; return this.desktopUpdateReported || this.olderShellModuleUpdateReady || this.olderShellUpdateAvailable;
} }
get desktopUpdateReplacesShell(): boolean {
return (this.desktopUpdateReported && this.desktopUpdateReportedReplacesShell) || this.olderShellUpdateAvailable;
}
private get olderShellUpdateAvailable(): boolean { private get olderShellUpdateAvailable(): boolean {
const electronApi = getElectronAPI(); const electronApi = getElectronAPI();
if (electronApi == null || electronApi.desktopUpdate != null || electronApi.updaterInstall == null) { if (electronApi == null || electronApi.desktopUpdate != null || electronApi.updaterInstall == null) {
@@ -281,6 +286,7 @@ class Updater {
this.unsubscribeDesktopUpdate = desktopUpdate.onStateChanged((state) => { this.unsubscribeDesktopUpdate = desktopUpdate.onStateChanged((state) => {
runInAction(() => { runInAction(() => {
this.desktopUpdateReported = state.available; this.desktopUpdateReported = state.available;
this.desktopUpdateReportedReplacesShell = state.replacesShell === true;
this.desktopUpdateRunning = state.updating === true; this.desktopUpdateRunning = state.updating === true;
}); });
}); });
@@ -314,6 +320,7 @@ class Updater {
const state = await desktopUpdate.state(); const state = await desktopUpdate.state();
runInAction(() => { runInAction(() => {
this.desktopUpdateReported = state.available; this.desktopUpdateReported = state.available;
this.desktopUpdateReportedReplacesShell = state.replacesShell === true;
this.desktopUpdateRunning = state.updating === true; this.desktopUpdateRunning = state.updating === true;
}); });
} catch (error) { } catch (error) {
@@ -669,6 +676,7 @@ class Updater {
this.unsubscribeDesktopUpdate = null; this.unsubscribeDesktopUpdate = null;
} }
this.desktopUpdateReported = false; this.desktopUpdateReported = false;
this.desktopUpdateReportedReplacesShell = false;
this.desktopUpdateRunning = false; this.desktopUpdateRunning = false;
this.olderShellModuleUpdateReady = false; this.olderShellModuleUpdateReady = false;
this.olderShellUpdateDownloaded = false; this.olderShellUpdateDownloaded = false;
@@ -8,7 +8,7 @@ import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import {msg} from '@lingui/core/macro'; import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/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 {observer} from 'mobx-react-lite';
import {useCallback} from 'react'; 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_DESCRIPTOR, {version, productName: RuntimeConfig.productName})
: i18n._(CLICK_TO_RELOAD_AND_UPDATE_2_DESCRIPTOR, {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 ( return (
<Tooltip text={tooltip} position="bottom" data-flx="channel.channel-header-components.updater-icon.tooltip"> <Tooltip text={tooltip} position="bottom" data-flx="channel.channel-header-components.updater-icon.tooltip">
<FocusRing offset={-2} data-flx="channel.channel-header-components.updater-icon.focus-ring"> <FocusRing offset={-2} data-flx="channel.channel-header-components.updater-icon.focus-ring">
@@ -79,7 +83,7 @@ export const UpdaterIcon = observer(() => {
aria-label={tooltip} aria-label={tooltip}
data-flx="channel.channel-header-components.updater-icon.button.click" data-flx="channel.channel-header-components.updater-icon.button.click"
> >
<ArrowClockwiseIcon <Icon
weight="bold" weight="bold"
className={affordance === 'updating' ? styles.updateIconSpinning : styles.updateIcon} className={affordance === 'updating' ? styles.updateIconSpinning : styles.updateIcon}
data-flx="channel.channel-header-components.updater-icon.update-icon" data-flx="channel.channel-header-components.updater-icon.update-icon"
@@ -25,8 +25,8 @@ function deferred() {
describe('DesktopUpdateGate', () => { describe('DesktopUpdateGate', () => {
test('nothing is available and nothing starts before the bootstrap arms the gate', async () => { test('nothing is available and nothing starts before the bootstrap arms the gate', async () => {
assert.deepEqual(getDesktopUpdateState(), {available: false, updating: false}); assert.deepEqual(getDesktopUpdateState(), {available: false, updating: false, replacesShell: false});
assert.deepEqual(await checkDesktopUpdateNow(), {available: false, updating: false}); assert.deepEqual(await checkDesktopUpdateNow(), {available: false, updating: false, replacesShell: false});
assert.equal(startDesktopUpdate(), 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(), true);
assert.equal(startDesktopUpdate(), false, 'a second click while the update runs is a no-op'); assert.equal(startDesktopUpdate(), false, 'a second click while the update runs is a no-op');
assert.deepEqual( assert.deepEqual(
getDesktopUpdateState(), getDesktopUpdateState(),
{available: false, updating: true}, {available: false, updating: true, replacesShell: false},
'the affordance hides the moment it is clicked', 'the affordance hides the moment it is clicked',
); );
assert.deepEqual( assert.deepEqual(
await checkDesktopUpdateNow(), await checkDesktopUpdateNow(),
{available: false, updating: true}, {available: false, updating: true, replacesShell: false},
'a poll during the update changes nothing', 'a poll during the update changes nothing',
); );
@@ -62,11 +62,11 @@ describe('DesktopUpdateGate', () => {
await new Promise((resolve) => setImmediate(resolve)); await new Promise((resolve) => setImmediate(resolve));
assert.equal(starts, 1); assert.equal(starts, 1);
assert.deepEqual(getDesktopUpdateState(), {available: false, updating: false}); assert.deepEqual(getDesktopUpdateState(), {available: false, updating: false, replacesShell: false});
assert.deepEqual(states, [ assert.deepEqual(states, [
{available: true, updating: false}, {available: true, updating: false, replacesShell: false},
{available: false, updating: true}, {available: false, updating: true, replacesShell: false},
{available: false, updating: false}, {available: false, updating: false, replacesShell: false},
]); ]);
stop(); stop();
}); });
@@ -111,7 +111,7 @@ describe('DesktopUpdateGate', () => {
pending.resolve(); pending.resolve();
await background; 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]); assert.deepEqual(calls, [false, true]);
}); });
@@ -127,7 +127,7 @@ describe('DesktopUpdateGate', () => {
assert.equal(startDesktopUpdate(), true); assert.equal(startDesktopUpdate(), true);
await new Promise((resolve) => setImmediate(resolve)); 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); assert.equal(startDesktopUpdate(), true);
}); });
}); });
+12 -2
View File
@@ -24,6 +24,7 @@ interface DesktopUpdateGateState {
checking: {readonly manual: boolean; readonly settled: Promise<void>} | null; checking: {readonly manual: boolean; readonly settled: Promise<void>} | null;
notifiedAvailable: boolean; notifiedAvailable: boolean;
notifiedUpdating: boolean; notifiedUpdating: boolean;
notifiedReplacesShell: boolean;
readonly listeners: Set<DesktopUpdateStateListener>; readonly listeners: Set<DesktopUpdateStateListener>;
} }
@@ -34,6 +35,7 @@ const gateState: DesktopUpdateGateState = {
checking: null, checking: null,
notifiedAvailable: false, notifiedAvailable: false,
notifiedUpdating: false, notifiedUpdating: false,
notifiedReplacesShell: false,
listeners: new Set(), listeners: new Set(),
}; };
@@ -42,9 +44,11 @@ function isAvailable(check: DesktopUpdateCheck | null): boolean {
} }
export function getDesktopUpdateState(): DesktopUpdateState { export function getDesktopUpdateState(): DesktopUpdateState {
const available = gateState.running == null && isAvailable(gateState.lastCheck);
return { return {
available: gateState.running == null && isAvailable(gateState.lastCheck), available,
updating: gateState.running != null, updating: gateState.running != null,
replacesShell: available && gateState.lastCheck?.shellNewer === true,
}; };
} }
@@ -55,11 +59,17 @@ export function desktopUpdateReplacesShell(): boolean {
function notify(): void { function notify(): void {
const state = getDesktopUpdateState(); const state = getDesktopUpdateState();
const updating = state.updating === true; 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; return;
} }
gateState.notifiedAvailable = state.available; gateState.notifiedAvailable = state.available;
gateState.notifiedUpdating = updating; gateState.notifiedUpdating = updating;
gateState.notifiedReplacesShell = replacesShell;
for (const listener of Array.from(gateState.listeners)) { for (const listener of Array.from(gateState.listeners)) {
try { try {
listener(state); listener(state);
@@ -57,6 +57,7 @@ export interface DesktopModuleAPI {
export interface DesktopUpdateState { export interface DesktopUpdateState {
readonly available: boolean; readonly available: boolean;
readonly updating?: boolean; readonly updating?: boolean;
readonly replacesShell?: boolean;
} }
export interface DesktopUpdateAPI { export interface DesktopUpdateAPI {