From b0de85d21abaadb74eb3b389a8f468e543e2e665 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 11 Jan 2021 11:01:01 -0500 Subject: [PATCH] add auto-update notifications and confirmation Signed-off-by: Sebastian Malton Show single update notification (#1985) Signed-off-by: Jari Kolehmainen Moving notification icons to top (#1987) Signed-off-by: Alex Andreev Switch to EventEmitter (producer&consumer) model - Main now broadcasts messages whenever updates are available and waits for the first response back - Add `onCorrect` and `onceCorrect` to ipc module for typechecking ipc messages - Correctly port styling from 3.6 Signed-off-by: Sebastian Malton --- package.json | 2 +- src/common/ipc.ts | 85 ---------- src/common/ipc/index.ts | 2 + src/common/ipc/ipc.ts | 146 ++++++++++++++++++ src/common/ipc/update-available/index.ts | 48 ++++++ src/common/utils/delay.ts | 12 +- src/common/utils/index.ts | 1 + src/main/app-updater.ts | 83 ++++++++-- src/main/index.ts | 7 +- src/main/tray.ts | 16 +- src/main/window-manager.ts | 6 +- .../components/button/button-panel.scss | 3 + .../components/button/button-panel.tsx | 13 ++ src/renderer/components/button/button.scss | 7 + src/renderer/components/button/button.tsx | 10 +- src/renderer/components/button/index.ts | 1 + .../notifications/notifications.scss | 4 + ...tions.store.ts => notifications.store.tsx} | 1 + .../notifications/notifications.tsx | 13 +- src/renderer/ipc/index.tsx | 57 +++++++ src/renderer/lens-app.tsx | 3 + src/renderer/themes/theme-vars.scss | 6 +- 22 files changed, 388 insertions(+), 138 deletions(-) delete mode 100644 src/common/ipc.ts create mode 100644 src/common/ipc/index.ts create mode 100644 src/common/ipc/ipc.ts create mode 100644 src/common/ipc/update-available/index.ts create mode 100644 src/renderer/components/button/button-panel.scss create mode 100644 src/renderer/components/button/button-panel.tsx rename src/renderer/components/notifications/{notifications.store.ts => notifications.store.tsx} (95%) create mode 100644 src/renderer/ipc/index.tsx diff --git a/package.json b/package.json index a929c5b59b..95f4017362 100644 --- a/package.json +++ b/package.json @@ -212,6 +212,7 @@ "mobx-observable-history": "^1.0.3", "mobx-react": "^6.2.2", "mock-fs": "^4.12.0", + "moment": "^2.26.0", "node-pty": "^0.9.0", "npm": "^6.14.8", "openid-client": "^3.15.2", @@ -321,7 +322,6 @@ "jest-mock-extended": "^1.0.10", "make-plural": "^6.2.2", "mini-css-extract-plugin": "^0.9.0", - "moment": "^2.26.0", "node-loader": "^0.6.0", "node-sass": "^4.14.1", "nodemon": "^2.0.4", diff --git a/src/common/ipc.ts b/src/common/ipc.ts deleted file mode 100644 index c2f8562cf7..0000000000 --- a/src/common/ipc.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Inter-process communications (main <-> renderer) -// https://www.electronjs.org/docs/api/ipc-main -// https://www.electronjs.org/docs/api/ipc-renderer - -import { ipcMain, ipcRenderer, webContents, remote } from "electron"; -import { toJS } from "mobx"; -import logger from "../main/logger"; -import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames"; - -const subFramesChannel = "ipc:get-sub-frames"; - -export function handleRequest(channel: string, listener: (...args: any[]) => any) { - ipcMain.handle(channel, listener); -} - -export async function requestMain(channel: string, ...args: any[]) { - return ipcRenderer.invoke(channel, ...args); -} - -function getSubFrames(): ClusterFrameInfo[] { - return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); -} - -export async function broadcastMessage(channel: string, ...args: any[]) { - const views = (webContents || remote?.webContents)?.getAllWebContents(); - - if (!views) return; - - if (ipcRenderer) { - ipcRenderer.send(channel, ...args); - } else { - ipcMain.emit(channel, ...args); - } - - for (const view of views) { - const type = view.getType(); - - logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args }); - view.send(channel, ...args); - - try { - const subFrames: ClusterFrameInfo[] = ipcRenderer - ? await requestMain(subFramesChannel) - : getSubFrames(); - - for (const frameInfo of subFrames) { - view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); - } - } catch (error) { - logger.error("[IPC]: failed to send IPC message", { error }); - } - } -} - -export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) { - if (ipcRenderer) { - ipcRenderer.on(channel, listener); - } else { - ipcMain.on(channel, listener); - } - - return listener; -} - -export function unsubscribeFromBroadcast(channel: string, listener: (...args: any[]) => any) { - if (ipcRenderer) { - ipcRenderer.off(channel, listener); - } else { - ipcMain.off(channel, listener); - } -} - -export function unsubscribeAllFromBroadcast(channel: string) { - if (ipcRenderer) { - ipcRenderer.removeAllListeners(channel); - } else { - ipcMain.removeAllListeners(channel); - } -} - -export function bindBroadcastHandlers() { - handleRequest(subFramesChannel, () => { - return getSubFrames(); - }); -} diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts new file mode 100644 index 0000000000..20fb79d0b2 --- /dev/null +++ b/src/common/ipc/index.ts @@ -0,0 +1,2 @@ +export * from "./ipc"; +export * from "./update-available"; diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts new file mode 100644 index 0000000000..be96531afc --- /dev/null +++ b/src/common/ipc/ipc.ts @@ -0,0 +1,146 @@ +// Inter-process communications (main <-> renderer) +// https://www.electronjs.org/docs/api/ipc-main +// https://www.electronjs.org/docs/api/ipc-renderer + +import { ipcMain, ipcRenderer, webContents, remote } from "electron"; +import { toJS } from "mobx"; +import { EventEmitter } from "ws"; +import logger from "../../main/logger"; +import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; + +const subFramesChannel = "ipc:get-sub-frames"; + +export type HandlerEvent = Parameters[1]>[0]; +export type EventListener = (event: HandlerEvent, ...args: T) => any; + +export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { + ipcMain.handle(channel, listener); +} + +export async function requestMain(channel: string, ...args: any[]) { + return ipcRenderer.invoke(channel, ...args); +} + +/** + * Adds a listener to `source` that waits for the first IPC message with the correct + * argument data is sent. + * @param channel The channel to be listened on + * @param listener The function for the channel to be called if the args of the correct type + * @param verifier The function to be called to verify that the args are the correct type + */ +export function onceCorrect< + EM extends EventEmitter, + T extends any[], + L extends (event: HandlerEvent, ...args: T) => any +>( + source: EM, + channel: string | symbol, + listener: L, + verifier: (args: unknown[]) => args is T +): void { + function handler(event: HandlerEvent, ...args: unknown[]): void { + if (verifier(args)) { + source.removeListener(channel, handler); // remove immediately + + Promise.resolve(listener(event, ...args)) // might return a promise + .catch(error => logger.error("[IPC]: channel once handler threw error", { channel, error })); + } else { + logger.error("[IPC]: channel was sent to with invalid data", { channel, args }); + } + } + + source.on(channel, handler); +} + +/** + * Adds a listener to `source` that checks to verify the arguments before calling the handler. + * @param channel The channel to be listened on + * @param listener The function for the channel to be called if the args of the correct type + * @param verifier The function to be called to verify that the args are the correct type + */ +export function onCorrect< + EM extends EventEmitter, + T extends any[], + L extends (event: HandlerEvent, ...args: T) => any +>( + source: EM, + channel: string | symbol, + listener: L, + verifier: (args: unknown[]) => args is T +): void { + source.on(channel, (event, ...args: unknown[]) => { + if (verifier(args)) { + Promise.resolve(listener(event, ...args)) // might return a promise + .catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error })); + } else { + logger.error("[IPC]: channel was sent to with invalid data", { channel, args }); + } + }); +} + +function getSubFrames(): ClusterFrameInfo[] { + return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); +} + +export async function broadcastMessage(channel: string, ...args: any[]) { + const views = (webContents || remote?.webContents)?.getAllWebContents(); + + if (!views) return; + + if (ipcRenderer) { + ipcRenderer.send(channel, ...args); + } else { + ipcMain.emit(channel, ...args); + } + + for (const view of views) { + const type = view.getType(); + + logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args }); + view.send(channel, ...args); + + try { + const subFrames: ClusterFrameInfo[] = ipcRenderer + ? await requestMain(subFramesChannel) + : getSubFrames(); + + for (const frameInfo of subFrames) { + view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); + } + } catch (error) { + logger.error("[IPC]: failed to send IPC message", { error }); + } + } +} + +export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) { + if (ipcRenderer) { + ipcRenderer.on(channel, listener); + } else { + ipcMain.on(channel, listener); + } + + return listener; +} + +export function unsubscribeFromBroadcast(channel: string, listener: (...args: any[]) => any) { + if (ipcRenderer) { + ipcRenderer.off(channel, listener); + } else { + ipcMain.off(channel, listener); + } +} + +export function unsubscribeAllFromBroadcast(channel: string) { + if (ipcRenderer) { + ipcRenderer.removeAllListeners(channel); + } else { + ipcMain.removeAllListeners(channel); + } +} + +export function bindBroadcastHandlers() { + handleRequest(subFramesChannel, () => { + return getSubFrames(); + }); +} diff --git a/src/common/ipc/update-available/index.ts b/src/common/ipc/update-available/index.ts new file mode 100644 index 0000000000..1e3fcf1268 --- /dev/null +++ b/src/common/ipc/update-available/index.ts @@ -0,0 +1,48 @@ +import { UpdateInfo } from "electron-updater"; + +export const UpdateAvailableChannel = "update-available"; +export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]"; + +/** + * [, ] + */ +export type UpdateAvailableFromMain = [string, UpdateInfo]; + +export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain { + if (args.length !== 2) { + return false; + } + + if (typeof args[0] !== "string") { + return false; + } + + if (typeof args[1] !== "object" || args[1] === null) { + // TODO: improve this checking + return false; + } + + return true; +} + +export type BackchannelArg = { + doUpdate: false; +} | { + doUpdate: true; + now: boolean; +}; + +export type UpdateAvailableToBackchannel = [BackchannelArg]; + +export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel { + if (args.length !== 1) { + return false; + } + + if (typeof args[0] !== "object" || args[0] === null) { + // TODO: improve this checking + return false; + } + + return true; +} diff --git a/src/common/utils/delay.ts b/src/common/utils/delay.ts index 208e042759..7d0686d29b 100644 --- a/src/common/utils/delay.ts +++ b/src/common/utils/delay.ts @@ -1,6 +1,8 @@ -// Create async delay for provided timeout in milliseconds - -export async function delay(timeoutMs = 1000) { - if (!timeoutMs) return; - await new Promise(resolve => setTimeout(resolve, timeoutMs)); +/** + * Return a promise that will be resolved after at least `timeout` ms have + * passed + * @param timeout The number of milliseconds before resolving + */ +export function delay(timeout = 1000): Promise { + return new Promise(resolve => setTimeout(resolve, timeout)); } diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 942c675f0a..2b8147fad9 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -18,3 +18,4 @@ export * from "./openExternal"; export * from "./downloadFile"; export * from "./escapeRegExp"; export * from "./tar"; +export * from "./delay"; diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index dd9ed97e69..5ecc09fe48 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -1,20 +1,71 @@ -import { autoUpdater } from "electron-updater"; +import { autoUpdater, UpdateInfo } from "electron-updater"; import logger from "./logger"; +import { isDevelopment, isTestEnv } from "../common/vars"; +import { delay } from "../common/utils"; +import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc"; +import * as uuid from "uuid"; +import { ipcMain } from "electron"; -export class AppUpdater { - static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day - - static checkForUpdates() { - return autoUpdater.checkForUpdatesAndNotify(); - } - - constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) { - autoUpdater.logger = logger; - } - - public start() { - setInterval(AppUpdater.checkForUpdates, this.updateInterval); - - return AppUpdater.checkForUpdates(); +function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) { + if (arg.doUpdate) { + if (arg.now) { + logger.info(`${AutoUpdateLogPrefix}: User chose to update now`); + autoUpdater.downloadUpdate() + .then(() => autoUpdater.quitAndInstall()) + .catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download or install update`, { error })); + } else { + logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`); + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.downloadUpdate() + .catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download update`, { error })); + } + } else { + logger.info(`${AutoUpdateLogPrefix}: User chose not to update`); + } +} + +/** + * starts the automatic update checking + * @param interval milliseconds between interval to check on, defaults to 24h + */ +export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void { + if (isDevelopment || isTestEnv) { + return; + } + + autoUpdater.logger = logger; + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = false; + + autoUpdater + .on("update-available", (args: UpdateInfo) => { + try { + // use a UUID so that this back-channel is harder to discover + const backchannel = uuid.v4(); + + // make sure that the handler is in place before broadcasting (prevent race-condition) + onceCorrect(ipcMain, backchannel, handleAutoUpdateBackChannel, areArgsUpdateAvailableToBackchannel); + logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: args.version }); + broadcastMessage(UpdateAvailableChannel, backchannel, args); + } catch (error) { + logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error }); + } + }); + + async function helper() { + while (true) { + await checkForUpdates(); + await delay(interval); + } + } + + helper(); +} + +export async function checkForUpdates(): Promise { + try { + await autoUpdater.checkForUpdates(); + } catch (error) { + logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) }); } } diff --git a/src/main/index.ts b/src/main/index.ts index 2b7817f093..710681656e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -10,7 +10,6 @@ import path from "path"; import { LensProxy } from "./lens-proxy"; import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; -import { AppUpdater } from "./app-updater"; import { shellSync } from "./shell-sync"; import { getFreePort } from "./port"; import { mangleProxyEnv } from "./proxy-env"; @@ -27,6 +26,7 @@ import type { LensExtensionId } from "../extensions/lens-extension"; import { installDeveloperTools } from "./developer-tools"; import { filesystemProvisionerStore } from "./extension-filesystem"; import { bindBroadcastHandlers } from "../common/ipc"; +import { startUpdateChecking } from "./app-updater"; const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -70,10 +70,6 @@ app.on("ready", async () => { app.exit(); }); - const updater = new AppUpdater(); - - updater.start(); - registerFileProtocol("static", __static); await installDeveloperTools(); @@ -112,6 +108,7 @@ app.on("ready", async () => { extensionLoader.init(); extensionDiscovery.init(); windowManager = WindowManager.getInstance(proxyPort); + windowManager.whenLoaded.then(() => startUpdateChecking()); // call after windowManager to see splash earlier try { diff --git a/src/main/tray.ts b/src/main/tray.ts index 47f641ad72..b18d3ddac8 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,9 +1,9 @@ import path from "path"; import packageInfo from "../../package.json"; -import { dialog, Menu, NativeImage, Tray } from "electron"; +import { Menu, NativeImage, Tray } from "electron"; import { autorun } from "mobx"; import { showAbout } from "./menu"; -import { AppUpdater } from "./app-updater"; +import { checkForUpdates } from "./app-updater"; import { WindowManager } from "./window-manager"; import { clusterStore } from "../common/cluster-store"; import { workspaceStore } from "../common/workspace-store"; @@ -112,16 +112,8 @@ function createTrayMenu(windowManager: WindowManager): Menu { { label: "Check for updates", async click() { - const result = await AppUpdater.checkForUpdates(); - - if (!result) { - const browserWindow = await windowManager.ensureMainWindow(); - - dialog.showMessageBoxSync(browserWindow, { - message: "No updates available", - type: "info", - }); - } + await checkForUpdates(); + await windowManager.ensureMainWindow(); }, }, { type: "separator" }, diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index bf7458afa0..f481c6450f 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,5 +1,5 @@ import type { ClusterId } from "../common/cluster-store"; -import { observable } from "mobx"; +import { observable, when } from "mobx"; import { app, BrowserWindow, dialog, shell, webContents } from "electron"; import windowStateKeeper from "electron-window-state"; import { appEventBus } from "../common/event-bus"; @@ -15,6 +15,9 @@ export class WindowManager extends Singleton { protected windowState: windowStateKeeper.State; protected disposers: Record = {}; + @observable mainViewInitiallyLoaded = false; + whenLoaded = when(() => this.mainViewInitiallyLoaded); + @observable activeClusterId: ClusterId; constructor(protected proxyPort: number) { @@ -91,6 +94,7 @@ export class WindowManager extends Singleton { setTimeout(() => { appEventBus.emit({ name: "app", action: "start" }); }, 1000); + this.mainViewInitiallyLoaded = true; } catch (err) { dialog.showErrorBox("ERROR!", err.toString()); } diff --git a/src/renderer/components/button/button-panel.scss b/src/renderer/components/button/button-panel.scss new file mode 100644 index 0000000000..fc908eea0f --- /dev/null +++ b/src/renderer/components/button/button-panel.scss @@ -0,0 +1,3 @@ +.ButtonPannel button:not(:last-of-type) { + margin-right: $margin; +} diff --git a/src/renderer/components/button/button-panel.tsx b/src/renderer/components/button/button-panel.tsx new file mode 100644 index 0000000000..79d87791ba --- /dev/null +++ b/src/renderer/components/button/button-panel.tsx @@ -0,0 +1,13 @@ +import "./button-panel.scss"; +import React from "react"; + +export function ButtonPannel(props: React.DetailedHTMLProps, HTMLDivElement>) { + return ( + <> +
+
+ {props.children} +
+ + ); +} diff --git a/src/renderer/components/button/button.scss b/src/renderer/components/button/button.scss index b850a3be59..fe26c04ee4 100644 --- a/src/renderer/components/button/button.scss +++ b/src/renderer/components/button/button.scss @@ -21,10 +21,16 @@ &.primary { background: $buttonPrimaryBackground; } + &.accent { background: $buttonAccentBackground; } + &.light { + background-color: white; + color: #505050; + } + &.plain { color: inherit; background: transparent; @@ -45,6 +51,7 @@ &.outlined { color: inherit; background: transparent; + border: 1px solid; &.active, &:focus { diff --git a/src/renderer/components/button/button.tsx b/src/renderer/components/button/button.tsx index 9fa822b214..8bcb37bad4 100644 --- a/src/renderer/components/button/button.tsx +++ b/src/renderer/components/button/button.tsx @@ -8,6 +8,7 @@ export interface ButtonProps extends ButtonHTMLAttributes, TooltipDecorator waiting?: boolean; primary?: boolean; accent?: boolean; + light?: boolean; plain?: boolean; outlined?: boolean; hidden?: boolean; @@ -24,13 +25,16 @@ export class Button extends React.PureComponent { private button: HTMLButtonElement; render() { - const { className, waiting, label, primary, accent, plain, hidden, active, big, round, outlined, tooltip, children, ...props } = this.props; - const btnProps = props as Partial; + const { + className, waiting, label, primary, accent, plain, hidden, active, big, + round, outlined, tooltip, light, children, ...props + } = this.props; + const btnProps: Partial = props; if (hidden) return null; btnProps.className = cssNames("Button", className, { - waiting, primary, accent, plain, active, big, round, outlined + waiting, primary, accent, plain, active, big, round, outlined, light, }); const btnContent: ReactNode = ( diff --git a/src/renderer/components/button/index.ts b/src/renderer/components/button/index.ts index 98d55acde6..ca2091adc2 100644 --- a/src/renderer/components/button/index.ts +++ b/src/renderer/components/button/index.ts @@ -1 +1,2 @@ export * from "./button"; +export * from "./button-panel"; diff --git a/src/renderer/components/notifications/notifications.scss b/src/renderer/components/notifications/notifications.scss index 37d4990ee5..4b300edda6 100644 --- a/src/renderer/components/notifications/notifications.scss +++ b/src/renderer/components/notifications/notifications.scss @@ -42,5 +42,9 @@ box-shadow: 0 0 20px $boxShadow; } } + + .close { + margin-top: -2px; + } } } diff --git a/src/renderer/components/notifications/notifications.store.ts b/src/renderer/components/notifications/notifications.store.tsx similarity index 95% rename from src/renderer/components/notifications/notifications.store.ts rename to src/renderer/components/notifications/notifications.store.tsx index 55549b9066..45c1eb9a6b 100644 --- a/src/renderer/components/notifications/notifications.store.ts +++ b/src/renderer/components/notifications/notifications.store.tsx @@ -18,6 +18,7 @@ export interface Notification { message: NotificationMessage; status?: NotificationStatus; timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide + onClose?(): void; // additonal logic on when the notification times out or is closed by the "x" } @autobind() diff --git a/src/renderer/components/notifications/notifications.tsx b/src/renderer/components/notifications/notifications.tsx index 4ab297e289..0c1ac692cf 100644 --- a/src/renderer/components/notifications/notifications.tsx +++ b/src/renderer/components/notifications/notifications.tsx @@ -72,23 +72,26 @@ export class Notifications extends React.Component { return (
this.elem = e}> {notifications.map(notification => { - const { id, status } = notification; + const { id, status, onClose } = notification; const msgText = this.getMessage(notification); return (
addAutoHideTimer(id)} onMouseEnter={() => removeAutoHideTimer(id)}> -
+
{msgText}
-
+
remove(id))} + onClick={prevDefault(() => { + remove(id); + onClose?.(); + })} />
diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx new file mode 100644 index 0000000000..4b942b65c3 --- /dev/null +++ b/src/renderer/ipc/index.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { ipcRenderer, IpcRendererEvent } from "electron"; +import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg } from "../../common/ipc"; +import { Notifications, notificationsStore } from "../components/notifications"; +import { Button, ButtonPannel } from "../components/button"; +import { isMac } from "../../common/vars"; +import * as uuid from "uuid"; + +function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, updateInfo]: UpdateAvailableFromMain): void { + const notificationId = uuid.v4(); + + function sendToBackchannel(data: BackchannelArg): void { + notificationsStore.remove(notificationId); + console.log("sending to backchanel", { backchannel, data }); + ipcRenderer.send(backchannel, data); + } + + function renderYesButtons() { + if (isMac) { + /** + * auto-updater's "installOnQuit" is not applicable for macOS as per their docs. + * + * See: https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/AppUpdater.ts#L27-L32 + */ + return