Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions apps/desktop/src/updates/DesktopUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,29 @@ describe("DesktopUpdates", () => {
}),
);

it.effect("keeps the disabled reason available after configuration and channel changes", () => {
const harness = makeHarness({ env: { T3CODE_DISABLE_AUTO_UPDATE: "true" } });
const reason = "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting.";

return Effect.scoped(
Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;

const configured = yield* updates.getState;
assert.equal(configured.message, reason);

const changed = yield* updates.setChannel("nightly");
assert.equal(changed.channel, "nightly");
assert.equal(changed.enabled, false);
assert.equal(changed.status, "disabled");
assert.equal(changed.message, reason);
assert.equal(harness.sentStates.at(-1)?.message, reason);
assert.equal(harness.checkCount(), 0);
}),
).pipe(Effect.provide(harness.layer));
});

it.effect("subscribe delivers the latest state plus subsequent changes", () => {
const harness = makeHarness();

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 @@ -273,13 +273,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 @@ -468,8 +470,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 @@ -631,10 +631,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 @@ -1056,9 +1056,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 @@ -1127,10 +1127,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
33 changes: 33 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 @@ -391,3 +393,34 @@ 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.",
});
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,
});
});
});
16 changes: 16 additions & 0 deletions apps/web/src/components/desktopUpdate.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,19 @@ export function canCheckForUpdate(state: DesktopUpdateState | null): boolean {
state.status !== "checking" && state.status !== "downloading" && state.status !== "disabled"
);
}

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.";
}

export function getDesktopUpdateIdleTooltip(
state: Pick<DesktopUpdateState, "enabled" | "status" | "message"> | null,
) {
const disabledReason = getDesktopUpdateDisabledReason(state);
return disabledReason
? { title: "Updates unavailable", detail: disabledReason }
: { title: "Check for updates", detail: null };
}
3 changes: 2 additions & 1 deletion apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { MobileAppSettingsRow } from "./MobileAppSettingsRow";
import {
canCheckForUpdate,
getDesktopUpdateButtonTooltip,
getDesktopUpdateDisabledReason,
getDesktopUpdateInstallConfirmationMessage,
isDesktopUpdateButtonDisabled,
resolveDesktopUpdateButtonAction,
Expand Down Expand Up @@ -419,7 +420,7 @@ function AboutVersionSection() {
const description =
action === "download" || action === "install"
? "Update available."
: "Current version of the application.";
: (getDesktopUpdateDisabledReason(updateState) ?? "Current version of the application.");

return (
<>
Expand Down
16 changes: 13 additions & 3 deletions apps/web/src/components/sidebar/SidebarUpdatePill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getDesktopUpdateActionError,
getDesktopUpdateButtonTooltip,
getDesktopUpdateDownloadedVersion,
getDesktopUpdateIdleTooltip,
getDesktopUpdateInstallConfirmationMessage,
isDesktopUpdateButtonDisabled,
resolveDesktopUpdateButtonAction,
Expand Down Expand Up @@ -236,6 +237,8 @@ function SidebarUpdateControl() {
isDownloading,
showCheckIcon,
});
const idleTooltip =
!showUpdateDetails && !showCheckIcon ? getDesktopUpdateIdleTooltip(state) : null;
const tooltip = runsEverything
? everything.summary
: showUpdateDetails
Expand All @@ -244,7 +247,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 @@ -441,7 +444,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 @@ -520,7 +523,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