diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 065ac9c6ca..a8c5932b69 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -46,7 +46,6 @@ import setupSentryInjectable from "./start-main-application/runnables/setup-sent import setupShellInjectable from "./start-main-application/runnables/setup-shell.injectable"; import setupSyncingOfWeblinksInjectable from "./start-main-application/runnables/setup-syncing-of-weblinks.injectable"; import stopServicesAndExitAppInjectable from "./stop-services-and-exit-app.injectable"; -import trayInjectable from "./tray/tray.injectable"; import applicationMenuInjectable from "./menu/application-menu.injectable"; import isDevelopmentInjectable from "../common/vars/is-development.injectable"; import setupSystemCaInjectable from "./start-main-application/runnables/setup-system-ca.injectable"; @@ -81,6 +80,7 @@ import syncUpdateIsReadyToBeInstalledInjectable from "./electron-app/runnables/u import quitAndInstallUpdateInjectable from "./electron-app/features/quit-and-install-update.injectable"; import electronUpdaterIsActiveInjectable from "./electron-app/features/electron-updater-is-active.injectable"; import publishIsConfiguredInjectable from "./update-app/publish-is-configured.injectable"; +import checkForPlatformUpdatesInjectable from "./update-app/check-for-platform-updates.injectable"; export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { const { @@ -125,7 +125,6 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { di.override(stopServicesAndExitAppInjectable, () => () => {}); di.override(lensResourcesDirInjectable, () => "/irrelevant"); - di.override(trayInjectable, () => ({ start: () => {}, stop: () => {} })); di.override(applicationMenuInjectable, () => ({ start: () => {}, stop: () => {} })); // TODO: Remove usages of globally exported appEventBus to get rid of this @@ -226,6 +225,10 @@ const overrideElectronFeatures = (di: DiContainer) => { di.override(syncUpdateIsReadyToBeInstalledInjectable, () => ({ start: () => {}, stop: () => {} })); di.override(quitAndInstallUpdateInjectable, () => () => {}); + di.override(checkForPlatformUpdatesInjectable, () => () => { + throw new Error("Tried to check for platform updates without explicit override."); + }); + di.override(createElectronWindowForInjectable, () => () => async () => ({ show: () => {}, diff --git a/src/main/tray/electron-tray/electron-tray.injectable.ts b/src/main/tray/electron-tray/electron-tray.injectable.ts new file mode 100644 index 0000000000..47572d59f8 --- /dev/null +++ b/src/main/tray/electron-tray/electron-tray.injectable.ts @@ -0,0 +1,116 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { Menu, Tray } from "electron"; +import packageJsonInjectable from "../../../common/vars/package-json.injectable"; +import logger from "../../logger"; +import { TRAY_LOG_PREFIX } from "../tray"; +import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable"; +import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token"; +import { pipeline } from "@ogre-tools/fp"; +import { isEmpty, map, filter } from "lodash/fp"; +import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import trayIconPathInjectable from "../tray-icon-path.injectable"; + +const electronTrayInjectable = getInjectable({ + id: "electron-tray", + + instantiate: (di) => { + const packageJson = di.inject(packageJsonInjectable); + const showApplicationWindow = di.inject(showApplicationWindowInjectable); + const isWindows = di.inject(isWindowsInjectable); + const logger = di.inject(loggerInjectable); + const trayIconPath = di.inject(trayIconPathInjectable); + + let tray: Tray; + + return { + start: () => { + tray = new Tray(trayIconPath); + + tray.setToolTip(packageJson.description); + tray.setIgnoreDoubleClickEvents(true); + + if (isWindows) { + tray.on("click", () => { + showApplicationWindow() + .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); + }); + } + }, + + stop: () => { + tray.destroy(); + }, + + setMenuItems: (items: TrayMenuItem[]) => { + pipeline( + items, + convertToElectronMenuTemplate, + Menu.buildFromTemplate, + + (template) => { + tray.setContextMenu(template); + }, + ); + }, + }; + }, + + causesSideEffects: true, +}); + +export default electronTrayInjectable; + +const convertToElectronMenuTemplate = (trayMenuItems: TrayMenuItem[]) => { + const _toTrayMenuOptions = (parentId: string | null) => + pipeline( + trayMenuItems, + + filter((item) => item.parentId === parentId), + + map( + (trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => { + if (trayMenuItem.separator) { + return { id: trayMenuItem.id, type: "separator" }; + } + + const childItems = _toTrayMenuOptions(trayMenuItem.id); + + return { + id: trayMenuItem.id, + label: trayMenuItem.label.get(), + enabled: trayMenuItem.enabled.get(), + toolTip: trayMenuItem.tooltip, + + ...(isEmpty(childItems) + ? { + type: "normal", + submenu: _toTrayMenuOptions(trayMenuItem.id), + + click: () => { + try { + trayMenuItem.click?.(); + } catch (error) { + logger.error( + `${TRAY_LOG_PREFIX}: clicking item "${trayMenuItem.id} failed."`, + { error }, + ); + } + }, + } + : { + type: "submenu", + submenu: _toTrayMenuOptions(trayMenuItem.id), + }), + + }; + }, + ), + ); + + return _toTrayMenuOptions(null); +}; diff --git a/src/main/tray/electron-tray/start-tray.injectable.ts b/src/main/tray/electron-tray/start-tray.injectable.ts new file mode 100644 index 0000000000..1a223ac3a5 --- /dev/null +++ b/src/main/tray/electron-tray/start-tray.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import electronTrayInjectable from "./electron-tray.injectable"; + +const startTrayInjectable = getInjectable({ + id: "start-tray", + + instantiate: (di) => { + const electronTray = di.inject(electronTrayInjectable); + + return { + run: () => { + electronTray.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startTrayInjectable; diff --git a/src/main/tray/electron-tray/stop-tray.injectable.ts b/src/main/tray/electron-tray/stop-tray.injectable.ts new file mode 100644 index 0000000000..f66ffb3a64 --- /dev/null +++ b/src/main/tray/electron-tray/stop-tray.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import electronTrayInjectable from "./electron-tray.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; +import stopReactiveTrayMenuItemsInjectable from "../reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable"; + +const stopTrayInjectable = getInjectable({ + id: "stop-tray", + + instantiate: (di) => { + const electronTray = di.inject(electronTrayInjectable); + + return { + run: () => { + electronTray.stop(); + }, + + runAfter: di.inject(stopReactiveTrayMenuItemsInjectable), + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopTrayInjectable; diff --git a/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts new file mode 100644 index 0000000000..b11654393a --- /dev/null +++ b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import { autorun } from "mobx"; +import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable"; +import electronTrayInjectable from "../electron-tray/electron-tray.injectable"; + +const reactiveTrayMenuItemsInjectable = getInjectable({ + id: "reactive-tray-menu-items", + + instantiate: (di) => { + const electronTray = di.inject(electronTrayInjectable); + const trayMenuItems = di.inject(trayMenuItemsInjectable); + + return getStartableStoppable("reactive-tray-menu-items", () => autorun(() => { + electronTray.setMenuItems(trayMenuItems.get()); + })); + }, +}); + +export default reactiveTrayMenuItemsInjectable; diff --git a/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts new file mode 100644 index 0000000000..63025e6a9a --- /dev/null +++ b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import reactiveTrayMenuItemsInjectable from "./reactive-tray-menu-items.injectable"; +import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import startTrayInjectable from "../electron-tray/start-tray.injectable"; + +const startReactiveTrayMenuItemsInjectable = getInjectable({ + id: "start-reactive-tray-menu-items", + + instantiate: (di) => { + const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable); + + return { + run: async () => { + await reactiveTrayMenuItems.start(); + }, + + runAfter: di.inject(startTrayInjectable), + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startReactiveTrayMenuItemsInjectable; diff --git a/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts new file mode 100644 index 0000000000..384cdc253a --- /dev/null +++ b/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import reactiveTrayMenuItemsInjectable from "./reactive-tray-menu-items.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; + +const stopReactiveTrayMenuItemsInjectable = getInjectable({ + id: "stop-reactive-tray-menu-items", + + instantiate: (di) => { + const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable); + + return { + run: async () => { + await reactiveTrayMenuItems.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopReactiveTrayMenuItemsInjectable; diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index 828ef81f27..c2fe44d452 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -47,6 +47,7 @@ import historyInjectable from "../../navigation/history.injectable"; import trayMenuItemsInjectable from "../../../main/tray/tray-menu-item/tray-menu-items.injectable"; import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token"; import updateIsAvailableStateInjectable from "../../../main/update-app/update-is-ready-to-be-installed-state.injectable"; +import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable"; type Callback = (dis: DiContainers) => void | Promise; @@ -150,6 +151,17 @@ export const getApplicationBuilder = () => { computed(() => []), ); + let trayMenuItemsStateFake: TrayMenuItem[]; + + mainDi.override(electronTrayInjectable, () => ({ + start: () => {}, + stop: () => {}, + + setMenuItems: (items) => { + trayMenuItemsStateFake = items; + }, + })); + let allowedResourcesState: IObservableArray; let rendered: RenderResult; @@ -202,26 +214,18 @@ export const getApplicationBuilder = () => { tray: { get: (id: string) => { - const trayMenuItems = mainDi.inject( - trayMenuItemsInjectable, - ); - - return trayMenuItems.get().find(matches({ id })); + return trayMenuItemsStateFake.find(matches({ id })); }, click: async (id: string) => { - const trayMenuItems = mainDi.inject( - trayMenuItemsInjectable, - ); - const menuItem = pipeline( - trayMenuItems.get(), + trayMenuItemsStateFake, find((menuItem) => menuItem.id === id), ); if (!menuItem) { const availableIds = pipeline( - trayMenuItems.get(), + trayMenuItemsStateFake, filter(item => !!item.click), map(item => item.id), join(", "),