fix(desktop): update from the splash only after the user clicks (#3280)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 17:25:34 +02:00
1 parent f1373a63c3
commit 7937d5f802
90 files changed
+6888 -11255

No files matched your search

@@ -559,29 +559,11 @@
position: relative;
}
.updateIconButtonDisabled {
composes: updateIconButton;
opacity: 0.4;
pointer-events: none;
}
.updateIcon {
height: 1.5rem;
width: 1.5rem;
}
.updateProgress {
position: absolute;
bottom: -0.4rem;
right: -0.6rem;
background: var(--background-tertiary);
color: var(--channel-header-text-primary);
font-size: 0.65rem;
padding: 0 0.25rem;
border-radius: 0.375rem;
border: 0.0625rem solid rgba(255, 255, 255, 0.08);
}
.updateIconError {
color: var(--status-danger);
}
@@ -1,59 +1,21 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
import Updater, {
DOWNLOADING_UPDATE_DESCRIPTOR as DOWNLOADING_UPDATE_STATUS_DESCRIPTOR,
} from '@app/features/app/state/Updater';
import Updater from '@app/features/app/state/Updater';
import styles from '@app/features/channel/components/ChannelHeader.module.css';
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
import {Platform} from '@app/features/platform/types/Platform';
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, DownloadSimpleIcon} from '@phosphor-icons/react';
import {ArrowClockwiseIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import {useCallback, useMemo} from 'react';
import {useCallback} from 'react';
const INSTALLING_UPDATE_DESCRIPTOR = msg({
message: 'Installing desktop update {version}…',
const UPDATE_AVAILABLE_DESCRIPTOR = msg({
message: 'Update available',
comment:
'Tooltip on the channel header updater icon while a Fluxer Desktop update is being installed. version is the target version. Trailing horizontal ellipsis is intentional.',
});
const INSTALLING_UPDATE_2_DESCRIPTOR = msg({
message: 'Installing desktop update…',
comment:
'Tooltip on the channel header updater icon while a Fluxer Desktop update is being installed when version is not known. Trailing horizontal ellipsis is intentional.',
});
const DOWNLOADING_UPDATE_AT_S_DESCRIPTOR = msg({
message: 'Downloading desktop update: {percent}% ({formatBytes} / {formatBytes2} at {formatBytes3}/s)',
comment:
'Tooltip on the channel header updater icon while downloading an update. Shows percent, bytes downloaded, total bytes, and bytes per second.',
});
const DOWNLOADING_UPDATE_DESCRIPTOR = msg({
message: 'Downloading desktop update {version}: {percent}%',
comment: 'Tooltip on the channel header updater icon while downloading a versioned update without throughput info.',
});
const DOWNLOADING_UPDATE_2_DESCRIPTOR = msg({
message: 'Downloading desktop update: {percent}%',
comment: 'Tooltip on the channel header updater icon while downloading an update with only percent available.',
});
const CLICK_TO_DOWNLOAD_UPDATE_DESCRIPTOR = msg({
message: 'Download desktop update {version} ({formatBytes})',
comment: 'Tooltip on the channel header updater icon prompting download of a versioned update with known size.',
});
const CLICK_TO_DOWNLOAD_UPDATE_2_DESCRIPTOR = msg({
message: 'Download desktop update ({formatBytes})',
comment:
'Tooltip on the channel header updater icon prompting download of an update of known size but unknown version.',
});
const CLICK_TO_DOWNLOAD_UPDATE_3_DESCRIPTOR = msg({
message: 'Download desktop update {version}',
comment: 'Tooltip on the channel header updater icon prompting download of a versioned update with unknown size.',
});
const CLICK_TO_DOWNLOAD_UPDATE_4_DESCRIPTOR = msg({
message: 'Download desktop update',
comment: 'Tooltip on the channel header updater icon prompting download of an update with no version or size info.',
'Tooltip and accessible label on the channel header updater icon in the desktop app. Clicking it closes the window and opens the updater, which downloads and installs the update.',
});
const CHOOSE_LINUX_PACKAGE_DESCRIPTOR = msg({
message: 'Desktop update {version} available. Choose a Linux package.',
@@ -64,21 +26,6 @@ const CHOOSE_LINUX_PACKAGE_2_DESCRIPTOR = msg({
comment:
'Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known.',
});
const CLICK_TO_RESTART_AND_INSTALL_UPDATE_DESCRIPTOR = msg({
message: 'Restart {productName} to install desktop update {version}',
comment:
'Tooltip on the channel header updater icon prompting a restart to apply a downloaded versioned update. productName is the app name.',
});
const CLICK_TO_RESTART_AND_INSTALL_UPDATE_2_DESCRIPTOR = msg({
message: 'Restart {productName} to install desktop update',
comment:
'Tooltip on the channel header updater icon prompting a restart to apply a downloaded update of unknown version. productName is the app name.',
});
const CLICK_TO_RELOAD_FOR_MODULE_UPDATE_DESCRIPTOR = msg({
message: 'Reload {productName} to use the updated modules',
comment:
'Tooltip on the channel header updater icon prompting a reload to apply freshly downloaded desktop modules. productName is the app name.',
});
const CLICK_TO_RELOAD_AND_UPDATE_DESCRIPTOR = msg({
message: 'Reload {productName} to use web update {version}',
comment:
@@ -90,151 +37,54 @@ const CLICK_TO_RELOAD_AND_UPDATE_2_DESCRIPTOR = msg({
'Tooltip on the channel header updater icon prompting a web reload to apply an update of unknown version. productName is the app name.',
});
function formatBytes(locale: string, bytes: number): string {
const units = ['byte', 'kilobyte', 'megabyte', 'gigabyte'] as const;
let value = Number.isFinite(bytes) && bytes > 0 ? bytes : 0;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return getCachedNumberFormat(locale, {
style: 'unit',
unit: units[unitIndex],
unitDisplay: 'narrow',
maximumFractionDigits: value >= 100 || unitIndex === 0 ? 0 : 1,
}).format(value);
}
type UpdaterAffordance = 'desktop' | 'manual' | 'web' | null;
function resolveUpdaterVisibility(): {
readonly hasActionableNativeUpdate: boolean;
readonly hasActionableModuleUpdate: boolean;
readonly hasActionableWebUpdate: boolean;
} {
const hasActionableNativeUpdate =
Platform.isElectron &&
(Updater.nativeUpdateReady ||
Updater.nativeAwaitingDownload ||
Updater.nativeManualUpdateAvailable ||
Updater.nativeDownloadInFlight);
const hasActionableModuleUpdate = Platform.isElectron && Updater.moduleUpdateReady && !hasActionableNativeUpdate;
const hasActionableWebUpdate =
!!Updater.updateInfo.web.available && !hasActionableNativeUpdate && !hasActionableModuleUpdate;
return {hasActionableNativeUpdate, hasActionableModuleUpdate, hasActionableWebUpdate};
function resolveUpdaterAffordance(): UpdaterAffordance {
if (Platform.isElectron && Updater.desktopUpdateAvailable) return 'desktop';
if (Platform.isElectron && Updater.nativeManualUpdateAvailable) return 'manual';
if (Updater.updateInfo.web.available) return 'web';
return null;
}
export function isUpdaterIconVisible(): boolean {
const {hasActionableNativeUpdate, hasActionableModuleUpdate, hasActionableWebUpdate} = resolveUpdaterVisibility();
return hasActionableNativeUpdate || hasActionableModuleUpdate || hasActionableWebUpdate;
return resolveUpdaterAffordance() != null;
}
export const UpdaterIcon = observer(() => {
const {i18n} = useLingui();
const store = Updater;
const {hasActionableNativeUpdate, hasActionableModuleUpdate, hasActionableWebUpdate} = resolveUpdaterVisibility();
const tooltip = useMemo(() => {
const version = store.displayVersion;
if (hasActionableModuleUpdate) {
return i18n._(CLICK_TO_RELOAD_FOR_MODULE_UPDATE_DESCRIPTOR, {productName: PRODUCT_NAME});
}
if (Platform.isElectron && store.updateInfo.native.installing) {
return version ? i18n._(INSTALLING_UPDATE_DESCRIPTOR, {version}) : i18n._(INSTALLING_UPDATE_2_DESCRIPTOR);
}
if (Platform.isElectron && store.nativeDownloadInFlight) {
if (!store.nativeDownloadProgressSupported) {
return i18n._(DOWNLOADING_UPDATE_STATUS_DESCRIPTOR);
}
const progress = store.downloadProgress;
const percent = getCachedNumberFormat(i18n.locale).format(progress ? Math.round(progress.percent) : 0);
if (progress && progress.total > 0 && progress.bytesPerSecond > 0) {
return i18n._(DOWNLOADING_UPDATE_AT_S_DESCRIPTOR, {
percent,
formatBytes: formatBytes(i18n.locale, progress.transferred),
formatBytes2: formatBytes(i18n.locale, progress.total),
formatBytes3: formatBytes(i18n.locale, progress.bytesPerSecond),
});
}
return version
? i18n._(DOWNLOADING_UPDATE_DESCRIPTOR, {version, percent})
: i18n._(DOWNLOADING_UPDATE_2_DESCRIPTOR, {percent});
}
if (Platform.isElectron && (store.nativeAwaitingDownload || store.nativeManualUpdateAvailable)) {
if (store.nativeManualUpdateAvailable && store.nativeManualDownloadOptions.length > 0) {
return version ? i18n._(CHOOSE_LINUX_PACKAGE_DESCRIPTOR, {version}) : i18n._(CHOOSE_LINUX_PACKAGE_2_DESCRIPTOR);
}
const size = store.updateInfo.native.downloadSize;
if (version && size && size > 0) {
return i18n._(CLICK_TO_DOWNLOAD_UPDATE_DESCRIPTOR, {version, formatBytes: formatBytes(i18n.locale, size)});
}
if (size && size > 0) {
return i18n._(CLICK_TO_DOWNLOAD_UPDATE_2_DESCRIPTOR, {formatBytes: formatBytes(i18n.locale, size)});
}
return version
? i18n._(CLICK_TO_DOWNLOAD_UPDATE_3_DESCRIPTOR, {version})
: i18n._(CLICK_TO_DOWNLOAD_UPDATE_4_DESCRIPTOR);
}
if (Platform.isElectron && store.nativeUpdateReady) {
return version
? i18n._(CLICK_TO_RESTART_AND_INSTALL_UPDATE_DESCRIPTOR, {version, productName: PRODUCT_NAME})
: i18n._(CLICK_TO_RESTART_AND_INSTALL_UPDATE_2_DESCRIPTOR, {productName: PRODUCT_NAME});
}
return version
? i18n._(CLICK_TO_RELOAD_AND_UPDATE_DESCRIPTOR, {version, productName: PRODUCT_NAME})
: i18n._(CLICK_TO_RELOAD_AND_UPDATE_2_DESCRIPTOR, {productName: PRODUCT_NAME});
}, [
hasActionableModuleUpdate,
store.displayVersion,
store.downloadProgress,
store.nativeAwaitingDownload,
store.nativeDownloadInFlight,
store.nativeDownloadProgressSupported,
store.nativeManualDownloadOptions,
store.nativeManualUpdateAvailable,
store.nativeUpdateReady,
store.updateInfo.native.downloadSize,
store.updateInfo.native.installing,
i18n.locale,
]);
const handleClick = useCallback(() => {
void store.applyUpdate();
}, [store]);
if (!hasActionableNativeUpdate && !hasActionableModuleUpdate && !hasActionableWebUpdate) {
void Updater.applyUpdate();
}, []);
const affordance = resolveUpdaterAffordance();
if (affordance == null) {
return null;
}
const isInstalling = Platform.isElectron && store.updateInfo.native.installing;
const isDownloading = Platform.isElectron && store.nativeDownloadInFlight;
const buttonClass = isInstalling ? styles.updateIconButtonDisabled : styles.updateIconButton;
const Icon = hasActionableWebUpdate || hasActionableModuleUpdate ? ArrowClockwiseIcon : DownloadSimpleIcon;
const percentLabel =
isDownloading && store.nativeDownloadProgressSupported
? getCachedNumberFormat(i18n.locale, {style: 'percent', maximumFractionDigits: 0}).format(
(store.downloadProgress?.percent ?? 0) / 100,
)
: null;
const version = Updater.displayVersion;
let tooltip: string;
if (affordance === 'manual' && Updater.nativeManualDownloadOptions.length > 0) {
tooltip = version ? i18n._(CHOOSE_LINUX_PACKAGE_DESCRIPTOR, {version}) : i18n._(CHOOSE_LINUX_PACKAGE_2_DESCRIPTOR);
} else if (affordance !== 'web') {
tooltip = i18n._(UPDATE_AVAILABLE_DESCRIPTOR);
} else {
tooltip = version
? i18n._(CLICK_TO_RELOAD_AND_UPDATE_DESCRIPTOR, {version, productName: PRODUCT_NAME})
: i18n._(CLICK_TO_RELOAD_AND_UPDATE_2_DESCRIPTOR, {productName: PRODUCT_NAME});
}
return (
<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">
<button
type="button"
className={buttonClass}
className={styles.updateIconButton}
onClick={handleClick}
aria-label={tooltip}
aria-disabled={isInstalling || undefined}
data-flx="channel.channel-header-components.updater-icon.button.click"
>
<Icon
<ArrowClockwiseIcon
weight="bold"
className={styles.updateIcon}
data-flx="channel.channel-header-components.updater-icon.update-icon"
/>
{percentLabel != null && (
<span
className={styles.updateProgress}
data-flx="channel.channel-header-components.updater-icon.update-progress"
>
{percentLabel}
</span>
)}
</button>
</FocusRing>
</Tooltip>
@@ -73,26 +73,6 @@ const MEDIA_LOADING_STATE_DESCRIPTOR = msg({
message: 'Media loading state',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
});
const ANY_UPDATER_READY_DESCRIPTOR = msg({
message: 'Any updater ready',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
});
const NATIVE_UPDATER_READY_DESCRIPTOR = msg({
message: 'Native updater ready',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
});
const NATIVE_UPDATE_PROGRESS_DESCRIPTOR = msg({
message: 'Native update progress',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
});
const WEB_UPDATER_READY_DESCRIPTOR = msg({
message: 'Web updater ready',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
});
const UPDATER_STATE_DESCRIPTOR = msg({
message: 'Updater state',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
});
const SHOW_MYSELF_TYPING_DESCRIPTOR = msg({
message: 'Show myself typing',
comment: 'Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.',
@@ -297,7 +277,6 @@ const formatDeveloperOptionValue = <K extends keyof DeveloperOptionsState>(
case 'mockMatureContentGateReason':
case 'mockMatureMediaGateReason':
case 'mockTitlebarPlatformOverride':
case 'mockUpdaterState':
return String(value).replace(/_/g, ' ');
case 'premiumSinceOverride':
case 'premiumUntilOverride':
@@ -342,16 +321,6 @@ export const getDeveloperOptionLabel = (key: keyof DeveloperOptionsState): Messa
return EMBED_SKELETONS_DESCRIPTOR;
case 'forceMediaLoading':
return MEDIA_LOADING_STATE_DESCRIPTOR;
case 'forceUpdateReady':
return ANY_UPDATER_READY_DESCRIPTOR;
case 'forceNativeUpdateReady':
return NATIVE_UPDATER_READY_DESCRIPTOR;
case 'mockNativeUpdateProgress':
return NATIVE_UPDATE_PROGRESS_DESCRIPTOR;
case 'forceWebUpdateReady':
return WEB_UPDATER_READY_DESCRIPTOR;
case 'mockUpdaterState':
return UPDATER_STATE_DESCRIPTOR;
case 'showMyselfTyping':
return SHOW_MYSELF_TYPING_DESCRIPTOR;
case 'slowAttachmentUpload':
@@ -12,11 +12,6 @@ export const DEFAULT_DEVELOPER_OPTIONS = {
forceRenderPlaceholders: false,
forceEmbedSkeletons: false,
forceMediaLoading: false,
forceUpdateReady: false,
forceNativeUpdateReady: false,
mockNativeUpdateProgress: null,
forceWebUpdateReady: false,
mockUpdaterState: 'none',
showMyselfTyping: false,
slowAttachmentUpload: false,
slowMessageLoad: false,