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
40 changes: 30 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -608,12 +608,18 @@ jobs:
shell: bash
env:
RELEASE_SCOPE: ${{ needs.prepare-release.outputs.release_scope }}
IS_PRERELEASE: ${{ needs.prepare-release.outputs.prerelease }}
run: |
set -euo pipefail
shopt -s nullglob

mkdir -p release-assets/upload

if [ "$IS_PRERELEASE" = "true" ] && [ "$RELEASE_SCOPE" != "all" ]; then
echo "Prerelease beta updates must include all platforms so every opted-in client can resolve beta metadata."
exit 1
fi

required_metadata=(
release-assets/windows-x64/latest.yml
release-assets/linux-x64/latest-linux.yml
Expand All @@ -630,6 +636,29 @@ jobs:
fi
done

if [ "$IS_PRERELEASE" = "true" ]; then
cp release-assets/windows-x64/latest.yml release-assets/windows-x64/beta.yml
cp release-assets/linux-x64/latest-linux.yml release-assets/linux-x64/beta-linux.yml
update_metadata=(
release-assets/windows-x64/beta.yml
release-assets/linux-x64/beta-linux.yml
)

if [ "$RELEASE_SCOPE" = "all" ]; then
cp release-assets/macos-merged/latest-mac.yml release-assets/macos-merged/beta-mac.yml
update_metadata+=(release-assets/macos-merged/beta-mac.yml)
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
else
update_metadata=(
release-assets/windows-x64/latest.yml
release-assets/linux-x64/latest-linux.yml
)

if [ "$RELEASE_SCOPE" = "all" ]; then
update_metadata+=(release-assets/macos-merged/latest-mac.yml)
fi
fi

assets=(
release-assets/windows-x64/*.exe
release-assets/windows-x64/*.blockmap
Expand All @@ -648,16 +677,7 @@ jobs:
)
fi

assets+=(
release-assets/windows-x64/latest.yml
release-assets/linux-x64/latest-linux.yml
)

if [ "$RELEASE_SCOPE" = "all" ]; then
assets+=(
release-assets/macos-merged/latest-mac.yml
)
fi
assets+=("${update_metadata[@]}")

if [ ${#assets[@]} -eq 0 ]; then
echo "No release assets found to checksum."
Expand Down
1 change: 1 addition & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ interface UpdateToastState {
phase: "available" | "downloading" | "ready" | "error";
delayMs: number;
isPreview?: boolean;
isExperimental?: boolean;
progressPercent?: number;
transferredBytes?: number;
totalBytes?: number;
Expand Down
141 changes: 23 additions & 118 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
webContents as electronWebContents,
ipcMain,
Menu,
Notification,
nativeImage,
session,
shell,
Expand All @@ -30,7 +29,6 @@ import { ensureMediaServer } from "./mediaServer";
import { hardenWebContentsNavigation, shouldHardenWebContentsType } from "./navigationPolicy";
import { shouldGrantDisplayCapture, shouldGrantMediaPermission } from "./permissionPolicy";
import { ensurePackagedRendererServer, getPackagedRendererBaseUrl } from "./rendererServer";
import type { UpdateToastPayload } from "./updater";
import {
checkForAppUpdates,
deferUpdateReminder,
Expand Down Expand Up @@ -179,8 +177,6 @@ let editorHasUnsavedChanges = false;
let isForceClosing = false;
let isCreatingMainWindow = false;
let isCreatingEditorWindow = false;
let activeUpdateNotification: Notification | null = null;
let activeUpdateNotificationKey: string | null = null;
const shouldEnforceSingleInstanceLock = !IS_DEV;
const hasSingleInstanceLock = shouldEnforceSingleInstanceLock
? app.requestSingleInstanceLock()
Expand Down Expand Up @@ -256,6 +252,14 @@ function getRecordingTrayIcon() {
}

function showHudOverlayFromTray() {
const updateToast = getUpdateToastWindow();
if (updateToast?.isVisible()) {
updateToast.show();
updateToast.moveTop();
updateToast.focus();
return true;
}

const hud = getHudOverlayWindow();
if (!hud) {
return false;
Expand Down Expand Up @@ -327,6 +331,14 @@ function focusOrCreateMainWindow() {
return;
}

const updateToast = getUpdateToastWindow();
if (updateToast?.isVisible()) {
updateToast.show();
updateToast.moveTop();
updateToast.focus();
return;
}

if (!mainWindow || mainWindow.isDestroyed()) {
const existingHud = getHudOverlayWindow();
if (existingHud && !existingHud.isDestroyed()) {
Expand Down Expand Up @@ -559,124 +571,12 @@ function syncDockIcon() {
}
}

function getUpdateNotificationTitle(payload: UpdateToastPayload) {
switch (payload.phase) {
case "available":
return `Recordly ${payload.version} is available`;
case "downloading":
return `Downloading Recordly ${payload.version}`;
case "ready":
return `Recordly ${payload.version} is ready`;
case "error":
return `Recordly ${payload.version} needs attention`;
}
}

function getUpdateNotificationBody(payload: UpdateToastPayload) {
switch (payload.phase) {
case "available":
return "Click to install the update and restart Recordly.";
case "downloading":
return "Recordly is downloading the update and will restart when it is ready.";
case "ready":
return "Click to install the downloaded update and restart.";
case "error":
return payload.primaryAction === "install-and-restart"
? "Click to try the install again."
: "Click to retry checking for updates.";
}
}

function clearActiveUpdateNotification() {
if (activeUpdateNotification) {
activeUpdateNotification.close();
activeUpdateNotification = null;
}
activeUpdateNotificationKey = null;
}

function sendUpdateToastToWindows(channel: "update-toast-state", payload: unknown) {
if (process.platform !== "darwin") {
if (!payload) {
clearActiveUpdateNotification();
return true;
}

const updatePayload = payload as UpdateToastPayload;
if (updatePayload.phase === "downloading") {
return true;
}

if (!Notification.isSupported()) {
return false;
}

const notificationKey = [
updatePayload.phase,
updatePayload.version,
updatePayload.detail,
].join(":");
if (activeUpdateNotificationKey === notificationKey) {
return true;
}

clearActiveUpdateNotification();
const notification = new Notification({
title: getUpdateNotificationTitle(updatePayload),
body: getUpdateNotificationBody(updatePayload),
icon: getAppImage(getPlatformAppIconFilename(128)),
silent: false,
});

notification.on("click", () => {
focusOrCreateMainWindow();
switch (updatePayload.phase) {
case "available":
void downloadAvailableUpdate(sendUpdateToastToWindows, {
installAfterDownload: true,
});
break;
case "ready":
installDownloadedUpdateNow(sendUpdateToastToWindows);
break;
case "error":
if (updatePayload.primaryAction === "install-and-restart") {
void downloadAvailableUpdate(sendUpdateToastToWindows, {
installAfterDownload: true,
});
} else {
void checkForAppUpdates(getUpdateDialogWindow, { manual: true });
}
break;
default:
break;
}
});

notification.on("close", () => {
if (activeUpdateNotification === notification) {
activeUpdateNotification = null;
activeUpdateNotificationKey = null;
}
});

notification.show();
// On Win10, showing a native notification can break setIgnoreMouseEvents
// forwarding on the transparent HUD overlay. Re-assert it after a short
// delay so the renderer's hover detection keeps working.
reassertHudOverlayMouseState();
activeUpdateNotification = notification;
activeUpdateNotificationKey = notificationKey;
return true;
}

if (!payload) {
const existingWindow = getUpdateToastWindow();
if (!existingWindow) {
return false;
if (existingWindow) {
existingWindow.webContents.send(channel, null);
}

existingWindow.webContents.send(channel, null);
hideUpdateToastWindow();
return true;
}
Expand Down Expand Up @@ -1095,6 +995,11 @@ app.whenReady().then(async () => {

createWindow();
setupAutoUpdates(getUpdateDialogWindow, sendUpdateToastToWindows);
if (IS_DEV && process.env.RECORDLY_DEV_PREVIEW_UPDATE === "1") {
setTimeout(() => {
previewUpdateToast(sendUpdateToastToWindows);
}, 750);
}

// Register the display media handler so that renderer's getDisplayMedia()
// calls land on the pre-selected source without showing a system picker.
Expand Down
29 changes: 29 additions & 0 deletions electron/updateChannel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import {
EXPERIMENTAL_UPDATE_DESCRIPTION,
getUpdateChannelConfiguration,
} from "./updateChannel";

describe("getUpdateChannelConfiguration", () => {
it("keeps regular clients on stable metadata", () => {
expect(getUpdateChannelConfiguration(false)).toEqual({
channel: "latest",
allowPrerelease: false,
allowDowngrade: false,
});
});

it("uses beta metadata only after the client opts in", () => {
expect(getUpdateChannelConfiguration(true)).toEqual({
channel: "beta",
allowPrerelease: true,
allowDowngrade: false,
});
});

it("uses the approved experimental update description", () => {
expect(EXPERIMENTAL_UPDATE_DESCRIPTION).toBe(
"You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available.",
);
});
});
22 changes: 22 additions & 0 deletions electron/updateChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const STABLE_UPDATE_CHANNEL = "latest";
export const EXPERIMENTAL_UPDATE_CHANNEL = "beta";
export const EXPERIMENTAL_UPDATE_DESCRIPTION =
"You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available.";

export interface UpdateChannelConfiguration {
channel: typeof STABLE_UPDATE_CHANNEL | typeof EXPERIMENTAL_UPDATE_CHANNEL;
allowPrerelease: boolean;
allowDowngrade: false;
}

export function getUpdateChannelConfiguration(
experimentalUpdatesEnabled: boolean,
): UpdateChannelConfiguration {
return {
channel: experimentalUpdatesEnabled
? EXPERIMENTAL_UPDATE_CHANNEL
: STABLE_UPDATE_CHANNEL,
allowPrerelease: experimentalUpdatesEnabled,
allowDowngrade: false,
};
}
Loading
Loading