Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/desktop/src/updates/DesktopUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,14 @@ describe("DesktopUpdates", () => {

const deb = yield* linuxState("deb\n");
assert.equal(deb.status, "idle");
assert.equal(deb.message, null);

const unmarked = yield* linuxState(undefined);
assert.equal(unmarked.status, "disabled");
assert.equal(
unmarked.message,
"Automatic updates on Linux require the AppImage or the .deb package.",
);
}),
);

Expand Down
22 changes: 11 additions & 11 deletions apps/desktop/src/updates/DesktopUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,15 @@ function parseAppUpdateYml(raw: string): Effect.Effect<Option.Option<AppUpdateYm

function createBaseUpdateState(
channel: DesktopUpdateChannel,
enabled: boolean,
disabledReason: Option.Option<string>,
environment: DesktopEnvironment.DesktopEnvironment["Service"],
): DesktopUpdateState {
const enabled = Option.isNone(disabledReason);
return {
...createInitialDesktopUpdateState(environment.appVersion, environment.runtimeInfo, channel),
enabled,
status: enabled ? "idle" : "disabled",
message: Option.getOrNull(disabledReason),
};
}

Expand Down Expand Up @@ -472,8 +474,6 @@ export const make = Effect.gen(function* () {
});
});

const shouldEnableAutoUpdates = resolveDisabledReason.pipe(Effect.map(Option.isNone));

const checkForUpdates = Effect.fn("desktop.updates.checkForUpdates")(function* (
reason: string,
actionReservation: "acquire" | "held" = "acquire",
Expand Down Expand Up @@ -644,10 +644,10 @@ export const make = Effect.gen(function* () {
),
);

const baseUpdateState = (channel: DesktopUpdateChannel, enabled: boolean) =>
const baseUpdateState = (channel: DesktopUpdateChannel, disabledReason: Option.Option<string>) =>
Ref.get(notAppliedVersionRef).pipe(
Effect.map((notAppliedVersion) => ({
...createBaseUpdateState(channel, enabled, environment),
...createBaseUpdateState(channel, disabledReason, environment),
...Option.match(notAppliedVersion, {
onNone: () => ({}),
onSome: (version) => ({ notAppliedVersion: version }),
Expand Down Expand Up @@ -1061,9 +1061,9 @@ export const make = Effect.gen(function* () {
}

const settings = yield* desktopSettings.get;
const enabled = yield* shouldEnableAutoUpdates;
yield* setState(yield* baseUpdateState(settings.updateChannel, enabled));
if (!enabled) {
const disabledReason = yield* resolveDisabledReason;
yield* setState(yield* baseUpdateState(settings.updateChannel, disabledReason));
if (Option.isSome(disabledReason)) {
return;
}
yield* Ref.set(updaterConfiguredRef, true);
Expand Down Expand Up @@ -1132,10 +1132,10 @@ export const make = Effect.gen(function* () {
),
);

const enabled = yield* shouldEnableAutoUpdates;
yield* setState(yield* baseUpdateState(nextChannel, enabled));
const disabledReason = yield* resolveDisabledReason;
yield* setState(yield* baseUpdateState(nextChannel, disabledReason));

if (!enabled || !(yield* Ref.get(updaterConfiguredRef))) {
if (Option.isSome(disabledReason) || !(yield* Ref.get(updaterConfiguredRef))) {
return yield* Ref.get(updateStateRef);
}

Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/components/desktopUpdate.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
getArm64IntelBuildWarningDescription,
getDesktopUpdateActionError,
getDesktopUpdateButtonTooltip,
getDesktopUpdateDisabledReason,
getDesktopUpdateIdleTooltip,
getDesktopUpdateInstallConfirmationMessage,
getDesktopUpdateReleaseHistoryUrl,
getDesktopUpdateReleaseUrl,
Expand Down Expand Up @@ -367,3 +369,37 @@ describe("getDesktopUpdateButtonTooltip", () => {
);
});
});

describe("disabled update reason", () => {
const disabledState: DesktopUpdateState = {
...baseState,
enabled: false,
status: "disabled",
message: "Automatic updates are only available in packaged production builds.",
};

it("explains why checking is unavailable", () => {
expect(getDesktopUpdateDisabledReason(disabledState)).toBe(
"Automatic updates are only available in packaged production builds.",
);
expect(getDesktopUpdateIdleTooltip(disabledState)).toEqual({
title: "Updates unavailable",
detail: "Automatic updates are only available in packaged production builds.",
});
});

it("falls back to a generic reason when the desktop app sent none", () => {
expect(getDesktopUpdateDisabledReason({ ...disabledState, message: null })).toBe(
"Automatic updates are not available in this build.",
);
});

it("keeps the check prompt when updates are available", () => {
expect(getDesktopUpdateDisabledReason(baseState)).toBeNull();
expect(getDesktopUpdateDisabledReason(null)).toBeNull();
expect(getDesktopUpdateIdleTooltip(baseState)).toEqual({
title: "Check for updates",
detail: null,
});
});
});
21 changes: 21 additions & 0 deletions apps/web/src/components/desktopUpdate.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,24 @@ export function canCheckForUpdate(state: DesktopUpdateState | null): boolean {
state.status !== "checking" && state.status !== "downloading" && state.status !== "disabled"
);
}

/**
* Why update checks are off, or null when they are available. The desktop app puts
* the reason it computed into `message` when it builds the disabled state.
*/
export function getDesktopUpdateDisabledReason(
state: Pick<DesktopUpdateState, "enabled" | "status" | "message"> | null,
): string | null {
if (!state || (state.enabled && state.status !== "disabled")) return null;
return state.message?.trim() || "Automatic updates are not available in this build.";
}

/** Hover text for the update button when there is nothing to download or install. */
export function getDesktopUpdateIdleTooltip(
state: Pick<DesktopUpdateState, "enabled" | "status" | "message"> | null,
): { readonly title: string; readonly detail: string | null } {
const disabledReason = getDesktopUpdateDisabledReason(state);
return disabledReason
? { title: "Updates unavailable", detail: disabledReason }
: { title: "Check for updates", detail: null };
}
4 changes: 3 additions & 1 deletion apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { CliCommandSettingsRow } from "./CliCommandSettingsRow";
import {
canCheckForUpdate,
getDesktopUpdateButtonTooltip,
getDesktopUpdateDisabledReason,
getDesktopUpdateInstallConfirmationMessage,
isDesktopUpdateButtonDisabled,
resolveDesktopUpdateButtonAction,
Expand Down Expand Up @@ -420,10 +421,11 @@ function AboutVersionSection() {
};
const buttonLabel =
actionLabel[action] ?? statusLabel[updateState?.status ?? ""] ?? "Check for Updates";
// A disabled button has pointer-events: none, so the row explains why checks are off.
const description =
action === "download" || action === "install"
? "Update available."
: "Current version of the application.";
: (getDesktopUpdateDisabledReason(updateState) ?? "Current version of the application.");

return (
<>
Expand Down
18 changes: 15 additions & 3 deletions apps/web/src/components/sidebar/SidebarUpdatePill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
getDesktopUpdateActionError,
getDesktopUpdateButtonTooltip,
getDesktopUpdateDownloadedVersion,
getDesktopUpdateIdleTooltip,
getDesktopUpdateInstallConfirmationMessage,
isDesktopUpdateButtonDisabled,
resolveDesktopUpdateButtonAction,
Expand Down Expand Up @@ -292,6 +293,10 @@ function SidebarUpdateControl() {
isDownloading,
showCheckIcon,
});
const idleTooltip =
runTooltip === null && !runsEverything && !showUpdateDetails && !showCheckIcon
? getDesktopUpdateIdleTooltip(state)
: null;
const tooltip =
runTooltip !== null
? runTooltip
Expand All @@ -303,7 +308,7 @@ function SidebarUpdateControl() {
: "Update available"
: showCheckIcon
? "Checking for updates…"
: "Check for updates";
: (idleTooltip?.title ?? "Check for updates");
const disabled = showCheckIcon
? true
: runsEverything
Expand Down Expand Up @@ -445,7 +450,7 @@ function SidebarUpdateControl() {
const updateButton = (
<button
type="button"
aria-label={tooltip}
aria-label={idleTooltip?.detail ? `${tooltip}: ${idleTooltip.detail}` : tooltip}
aria-disabled={isInteractionDisabled || undefined}
className={cn(
"inline-flex size-8 items-center justify-center rounded-full outline-hidden ring-ring transition-colors focus-visible:ring-2 focus-visible:ring-inset",
Expand Down Expand Up @@ -530,7 +535,14 @@ function SidebarUpdateControl() {
side="top"
variant={showUpdateDetails ? "glass" : "default"}
>
<span className="whitespace-pre-line">{tooltip}</span>
{idleTooltip?.detail ? (
<div className="text-left">
<div className="font-medium">{idleTooltip.title}</div>
<div className="text-muted-foreground">{idleTooltip.detail}</div>
</div>
) : (
<span className="whitespace-pre-line">{tooltip}</span>
)}
</TooltipPopup>
) : null}
</Tooltip>
Expand Down
Loading