From bbcc62103f7a3e987e1bd02ca774448983f1ece5 Mon Sep 17 00:00:00 2001 From: Kyle Rosenberg Date: Mon, 27 Dec 2021 19:20:12 -0500 Subject: [PATCH] heavily refactor ipc from remote hackary so everything works again a smaller change is probably waranted but i am lazy. closes #352 closes #351 closes #350 closes #347 closes #345 --- src/background.ts | 80 ++++++++-------- src/bridge.ts | 176 +++++++---------------------------- src/helpers/constants.ts | 11 +-- src/helpers/getMainWindow.ts | 8 ++ src/helpers/observers.ts | 71 ++++++++++++++ src/helpers/settings.ts | 2 - src/helpers/trayManager.ts | 32 ++++--- src/helpers/window.ts | 145 ----------------------------- src/index.d.ts | 17 ---- src/menu/contextMenu.ts | 5 +- src/menu/devMenu.ts | 5 +- src/menu/items/about.ts | 2 +- src/menu/trayMenu.ts | 5 +- 13 files changed, 189 insertions(+), 370 deletions(-) create mode 100644 src/helpers/getMainWindow.ts create mode 100644 src/helpers/observers.ts delete mode 100644 src/helpers/window.ts delete mode 100644 src/index.d.ts diff --git a/src/background.ts b/src/background.ts index 9ef0b54e..26d94c32 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,5 +1,7 @@ -import { app, Event as ElectronEvent, Menu, shell } from "electron"; +import { app, Event as ElectronEvent, ipcMain, Menu, shell } from "electron"; +import { BrowserWindow } from "electron/main"; import path from "path"; +import process from "process"; import { checkForUpdate } from "./helpers/autoUpdate"; import { IS_DEV, @@ -8,9 +10,9 @@ import { IS_WINDOWS, RESOURCES_PATH, } from "./helpers/constants"; +import { MenuManager } from "./helpers/menuManager"; import { settings } from "./helpers/settings"; -import { TrayManager } from "./helpers/trayManager"; -import { CustomBrowserWindow } from "./helpers/window"; +import { Conversation, TrayManager } from "./helpers/trayManager"; import { baseMenuTemplate } from "./menu/baseMenu"; import { popupContextMenu } from "./menu/contextMenu"; import { devMenuTemplate } from "./menu/devMenu"; @@ -25,7 +27,7 @@ const { checkForUpdateOnLaunchEnabled, } = settings; -let mainWindow: CustomBrowserWindow; +let mainWindow: BrowserWindow; let trayManager: TrayManager; app.on("second-instance", () => { @@ -40,25 +42,6 @@ if (!app.requestSingleInstanceLock()) { app.quit(); } -const setApplicationMenu = () => { - const menus = baseMenuTemplate; - if (IS_DEV) { - menus.push(devMenuTemplate); - } - menus.push(helpMenuTemplate); - Menu.setApplicationMenu(Menu.buildFromTemplate(menus)); -}; - -/** - * Save userData in separate folders for each environment. - * Thanks to this you can use production and development versions of the app - * on same machine like those are two separate apps. - */ -if (IS_DEV) { - const userDataPath = app.getPath("userData"); - app.setPath("userData", `${userDataPath}-(${process.env.NODE_ENV})`); -} - if (IS_WINDOWS) { app.setAppUserModelId("pw.kmr.android-messages-desktop"); app.setAsDefaultProtocolClient("android-messages-desktop"); @@ -67,13 +50,7 @@ if (IS_WINDOWS) { app.on("ready", () => { trayManager = new TrayManager(); - setApplicationMenu(); - - if (IS_MAC) { - app.on("activate", () => { - mainWindow.show(); - }); - } + new MenuManager(); if (checkForUpdateOnLaunchEnabled.value && !IS_DEV) { checkForUpdate(true); @@ -82,12 +59,13 @@ app.on("ready", () => { const { width, height } = savedWindowSize.value; const { x, y } = savedWindowPosition.value ?? {}; - mainWindow = new CustomBrowserWindow("main", { + mainWindow = new BrowserWindow({ width, height, x, y, autoHideMenuBar: autoHideMenuEnabled.value, + title: "Android Messages", show: false, //don't show window just yet (issue #229) icon: IS_LINUX ? path.resolve(RESOURCES_PATH, "icons", "128x128.png") @@ -95,11 +73,17 @@ app.on("ready", () => { webPreferences: { nodeIntegration: true, contextIsolation: false, - enableRemoteModule: true, - preload: path.resolve(app.getAppPath(), "bridge.js"), + preload: IS_DEV + ? path.resolve(app.getAppPath(), "bridge.js") + : path.resolve(app.getAppPath(), "app", "bridge.js"), }, }); + process.env.MAIN_WINDOW_ID = mainWindow.id.toString(); + + if (!(settings.trayEnabled.value && settings.startInTrayEnabled.value)) { + mainWindow.show(); + } // set user agent to potentially make google fi work const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"; @@ -114,14 +98,12 @@ app.on("ready", () => { }) ); - // Quick and dirty way for renderer process to access mainWindow for communication - app.mainWindow = mainWindow; - app.trayManager = trayManager; - app.settings = settings; - mainWindow.loadURL("https://messages.google.com/web/"); trayManager.startIfEnabled(); + settings.showIconsInRecentConversationTrayEnabled.subscribe(() => + trayManager.refreshTrayMenu() + ); let quitViaContext = false; app.on("before-quit", () => { @@ -173,3 +155,25 @@ app.on("ready", () => { } ); }); //onready + +ipcMain.on("should-hide-notification-content", (event) => { + event.returnValue = settings.hideNotificationContentEnabled.value; +}); + +ipcMain.on("show-main-window", (event) => { + mainWindow.show(); +}); + +ipcMain.on("flash-main-window-if-not-focused", (event) => { + if (!mainWindow.isFocused()) { + mainWindow.flashFrame(true); + } +}); + +ipcMain.on("set-unread-status", (event, unreadStatus: boolean) => { + trayManager.setUnread(unreadStatus); +}); + +ipcMain.on("set-recent-conversations", (event, data: Conversation[]) => { + trayManager.setRecentConversations(data); +}); diff --git a/src/bridge.ts b/src/bridge.ts index 9351e949..ce983d59 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -1,88 +1,19 @@ -import { remote, NotificationConstructorOptions } from "electron"; +import { ipcRenderer } from "electron"; import path from "path"; +import { INITIAL_ICON_IMAGE, RESOURCES_PATH } from "./helpers/constants"; import { - INITIAL_ICON_IMAGE, - IS_DEV, - RECENT_CONVERSATION_TRAY_COUNT, - RESOURCES_PATH, -} from "./helpers/constants"; + createRecentThreadObserver, + createUnreadObserver, + focusFunctions, + recentThreadObserver, +} from "./helpers/observers"; import { getProfileImg } from "./helpers/profileImage"; -import { popupContextMenu } from "./menu/contextMenu"; - -const { Notification: ElectronNotification, app, dialog } = remote; - -function unreadObserver() { - if (document.querySelector(".unread") != null) { - app.trayManager?.setUnread(true); - } else { - app.trayManager?.setUnread(false); - } -} - -function createUnreadObserver() { - const observer = new MutationObserver(unreadObserver); - observer.observe( - (document.body.querySelector( - "mws-conversations-list" - ) as unknown) as Element, - { - subtree: true, - attributes: true, - attributeFilter: ["data-e2e-is-unread"], - } - ); - return observer; -} - -function recentThreadObserver() { - const conversations = Array.from( - document.body.querySelectorAll("mws-conversation-list-item") - ).slice(0, RECENT_CONVERSATION_TRAY_COUNT); - - const data = conversations.map((conversation) => { - const name = conversation.querySelector("a div.text-content h3.name span") - ?.textContent; - const canvas = conversation.querySelector( - "a div.avatar-container canvas" - ) as HTMLCanvasElement | null; - - const image = canvas?.toDataURL(); - - const recentMessage = conversation.querySelector( - "a div.text-content div.snippet-text mws-conversation-snippet span" - )?.textContent; - - const click = () => void conversation.querySelector("a")?.click(); - - return { name, image, recentMessage, click }; - }); - app.trayManager?.setRecentConversations(data); -} - -function createRecentThreadObserver() { - const observer = new MutationObserver(recentThreadObserver); - observer.observe( - (document.body.querySelector( - "mws-conversations-list" - ) as unknown) as Element, - { - attributes: false, - subtree: true, - childList: true, - } - ); - return observer; -} window.addEventListener("load", () => { const conversationListObserver = new MutationObserver(() => { if (document.querySelector("mws-conversations-list") != null) { createUnreadObserver(); createRecentThreadObserver(); - app.settings?.showIconsInRecentConversationTrayEnabled.subscribe( - recentThreadObserver - ); - app.settings?.trayEnabled.subscribe(recentThreadObserver); // keep trying to get an image that isnt blank until they load const interval = setInterval(() => { @@ -95,14 +26,20 @@ window.addEventListener("load", () => { ) as HTMLCanvasElement | null; if (canvas != null && canvas.toDataURL() != INITIAL_ICON_IMAGE) { - console.log(canvas.toDataURL()); recentThreadObserver(); + // refresh for profile image loads after letter loads. + setTimeout(recentThreadObserver, 3000); clearInterval(interval); } } }, 250); conversationListObserver.disconnect(); } + + const title = document.head.querySelector("title"); + if (title != null) { + title.innerText = "Android Messages"; + } }); conversationListObserver.observe(document.body, { @@ -110,87 +47,40 @@ window.addEventListener("load", () => { subtree: true, childList: true, }); - - // a work around issue #229 (https://github.com/OrangeDrangon/android-messages-desktop/issues/229) - if ( - !(app.settings?.startInTrayEnabled.value && app.settings?.trayEnabled.value) - ) { - app.mainWindow?.show(); - } - - // Note: this hides this during dev - // remove the condition for testing - if (!IS_DEV && !app.settings?.seenResetSettingsWarning.value) { - const message = ` -The settings for this app have been reset. - -This is a one time occurance and is the result of behind the scenes work to clean up the code. - -You may notice three missing settings: - - - Enter to Send: Moved to the 3 dots menu - - Notification Sound: Moved to the 3 dots menu - - Use System Theme: Removed for the time being in favor of manual operation - `; - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Settings Reset", - message, - }); - app.settings?.seenResetSettingsWarning.next(true); - } }); -/** - * Override the webview's window's instance of the Notification class and forward their data to the - * main process. This is Necessary to generate and send a custom notification via Electron instead - * of just forwarding the webview (Google) ones. - * - * Derived from: - * https://github.com/electron/electron/blob/master/docs/api/ipc-main.md#sending-messages - * https://stackoverflow.com/questions/2891096/addeventlistener-using-apply - * https://stackoverflow.com/questions/31231622/event-listener-for-web-notification - * https://stackoverflow.com/questions/1421257/intercept-javascript-event - */ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment +const OldNotification = window.Notification; + // @ts-ignore window.Notification = function (title: string, options: NotificationOptions) { const icon = getProfileImg(title); - const notificationOpts: NotificationConstructorOptions = app.settings - ?.hideNotificationContentEnabled.value + const hideContent = ipcRenderer.sendSync("should-hide-notification-content"); + + const notificationOpts: NotificationOptions = hideContent ? { - title: "New Message", body: "Click to open", icon: path.resolve(RESOURCES_PATH, "icons", "64x64.png"), } : { - title, - icon, + icon: icon?.toDataURL(), body: options.body || "", }; - // let google handle making the noise - notificationOpts.silent = true; - - const notification = new ElectronNotification(notificationOpts); - notification.addListener("click", () => { - app.mainWindow?.show(); + const newTitle = hideContent ? "New Message" : title; + const notification = new OldNotification(newTitle, notificationOpts); + notification.addEventListener("click", () => { + ipcRenderer.send("show-main-window"); document.dispatchEvent(new Event("focus")); }); - // Mock the api for adding event listeners for a normal Browser notification - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - notification.addEventListener = notification.addListener; - notification.show(); - if (!app.mainWindow?.isFocused()) { - app.mainWindow?.flashFrame(true); - } + ipcRenderer.send("flash-main-window-if-not-focused"); return notification; }; -// THIS IS NEEDED FOR GOOGLE TO ISSUE NOTIFICATIONS -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -Notification.permission = "granted"; -Notification.requestPermission = async () => "granted"; + +window.Notification.requestPermission = async () => "granted"; +//@ts-ignore +window.Notification.permission = "granted"; + +ipcRenderer.on("focus-conversation", (event, i) => { + focusFunctions[i](); +}); diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index 256f035a..9ccdeeb1 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -9,14 +9,13 @@ export const IS_MAC = OS_NAME === "darwin"; export const IS_LINUX = OS_NAME === "linux"; // Environment and paths -export const IS_DEV = process.env.NODE_ENV === "development"; +export const IS_DEV = process.env.NODE_ENV == "development"; export const BASE_APP_PATH = path.resolve(__dirname, ".."); export const RESOURCES_PATH = path.resolve(BASE_APP_PATH, "resources"); // needs to be a function because app is not initialized yet otherwise? -export const SETTINGS_FILE = (): string => - !IS_DEV - ? path.resolve(app.getPath("userData"), `settings.json`) - : path.resolve(BASE_APP_PATH, "settings.json"); +export const SETTINGS_FILE = (): string => { + return path.resolve(app.getPath("userData"), `settings.json`); +}; // UUID /** @@ -28,6 +27,6 @@ export const UUID_NAMESPACE = "ddf09da3-3df8-4417-ae3b-62d3ed4bfb72"; * Initial image AMD loads for icons. Used to check against and ignore when populating tray context menu. */ export const INITIAL_ICON_IMAGE = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAEYklEQVR4Xu3UAQkAAAwCwdm/9HI83BLIOdw5AgQIRAQWySkmAQIEzmB5AgIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlACBB1YxAJfjJb2jAAAAAElFTkSuQmCC"; + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAAAXNSR0IArs4c6QAABGJJREFUeF7t1AEJAAAMAsHZv/RyPNwSyDncOQIECEQEFskpJgECBM5geQICBDICBitTlaAECBgsP0CAQEbAYGWqEpQAAYPlBwgQyAgYrExVghIgYLD8AAECGQGDlalKUAIEDJYfIEAgI2CwMlUJSoCAwfIDBAhkBAxWpipBCRAwWH6AAIGMgMHKVCUoAQIGyw8QIJARMFiZqgQlQMBg+QECBDICBitTlaAECBgsP0CAQEbAYGWqEpQAAYPlBwgQyAgYrExVghIgYLD8AAECGQGDlalKUAIEDJYfIEAgI2CwMlUJSoCAwfIDBAhkBAxWpipBCRAwWH6AAIGMgMHKVCUoAQIGyw8QIJARMFiZqgQlQMBg+QECBDICBitTlaAECBgsP0CAQEbAYGWqEpQAAYPlBwgQyAgYrExVghIgYLD8AAECGQGDlalKUAIEDJYfIEAgI2CwMlUJSoCAwfIDBAhkBAxWpipBCRAwWH6AAIGMgMHKVCUoAQIGyw8QIJARMFiZqgQlQMBg+QECBDICBitTlaAECBgsP0CAQEbAYGWqEpQAAYPlBwgQyAgYrExVghIgYLD8AAECGQGDlalKUAIEDJYfIEAgI2CwMlUJSoCAwfIDBAhkBAxWpipBCRAwWH6AAIGMgMHKVCUoAQIGyw8QIJARMFiZqgQlQMBg+QECBDICBitTlaAECBgsP0CAQEbAYGWqEpQAAYPlBwgQyAgYrExVghIgYLD8AAECGQGDlalKUAIEDJYfIEAgI2CwMlUJSoCAwfIDBAhkBAxWpipBCRAwWH6AAIGMgMHKVCUoAQIGyw8QIJARMFiZqgQlQMBg+QECBDICBitTlaAECBgsP0CAQEbAYGWqEpQAAYPlBwgQyAgYrExVghIgYLD8AAECGQGDlalKUAIEDJYfIEAgI2CwMlUJSoCAwfIDBAhkBAxWpipBCRAwWH6AAIGMgMHKVCUoAQIGyw8QIJARMFiZqgQlQMBg+QECBDICBitTlaAECBgsP0CAQEbAYGWqEpQAAYPlBwgQyAgYrExVghIgYLD8AAECGQGDlalKUAIEDJYfIEAgI2CwMlUJSoCAwfIDBAhkBAxWpipBCRAwWH6AAIGMgMHKVCUoAQIGyw8QIJARMFiZqgQlQMBg+QECBDICBitTlaAECBgsP0CAQEbAYGWqEpQAAYPlBwgQyAgYrExVghIgYLD8AAECGQGDlalKUAIEDJYfIEAgI2CwMlUJSoCAwfIDBAhkBAxWpipBCRAwWH6AAIGMgMHKVCUoAQIGyw8QIJARMFiZqgQlQMBg+QECBDICBitTlaAECBgsP0CAQEbAYGWqEpQAAYPlBwgQyAgYrExVghIgYLD8AAECGQGDlalKUAIEDJYfIEAgI2CwMlUJSoCAwfIDBAhkBAxWpipBCRAwWH6AAIGMgMHKVCUoAQIGyw8QIJARMFiZqgQlQMBg+QECBDICBitTlaAECBgsP0CAQEbAYGWqEpQAgQdWMQCX4yW9owAAAABJRU5ErkJggg=="; export const RECENT_CONVERSATION_TRAY_COUNT = 3; diff --git a/src/helpers/getMainWindow.ts b/src/helpers/getMainWindow.ts new file mode 100644 index 00000000..a5462905 --- /dev/null +++ b/src/helpers/getMainWindow.ts @@ -0,0 +1,8 @@ +import { BrowserWindow } from "electron"; + +export function getMainWindow(): BrowserWindow | null { + if (process.env.MAIN_WINDOW_ID != null) { + return BrowserWindow.fromId(Number(process.env.MAIN_WINDOW_ID)); + } + return null; +} diff --git a/src/helpers/observers.ts b/src/helpers/observers.ts new file mode 100644 index 00000000..64e9dc91 --- /dev/null +++ b/src/helpers/observers.ts @@ -0,0 +1,71 @@ +import { BrowserWindow, ipcRenderer } from "electron"; +import { RECENT_CONVERSATION_TRAY_COUNT } from "./constants"; +import { Conversation } from "./trayManager"; + +function unreadObserver() { + if (document.querySelector(".unread") != null) { + ipcRenderer.send("set-unread-status", true); + } else { + ipcRenderer.send("set-unread-status", false); + } +} + +export function createUnreadObserver(): MutationObserver { + const observer = new MutationObserver(unreadObserver); + observer.observe( + (document.body.querySelector( + "mws-conversations-list" + ) as unknown) as Element, + { + subtree: true, + attributes: true, + attributeFilter: ["data-e2e-is-unread"], + } + ); + return observer; +} + +export const focusFunctions = new Array(RECENT_CONVERSATION_TRAY_COUNT) + .fill(0) + .map(() => () => void 1); + +export function recentThreadObserver() { + const conversations = Array.from( + document.body.querySelectorAll("mws-conversation-list-item") + ).slice(0, RECENT_CONVERSATION_TRAY_COUNT); + + const data: Conversation[] = conversations.map((conversation, i) => { + const name = conversation.querySelector("a div.text-content h3.name span") + ?.textContent; + const canvas = conversation.querySelector( + "a div.avatar-container canvas" + ) as HTMLCanvasElement | null; + + const image = canvas?.toDataURL(); + + const recentMessage = conversation.querySelector( + "a div.text-content div.snippet-text mws-conversation-snippet span" + )?.textContent; + + const focusFunction = () => void conversation.querySelector("a")?.click(); + focusFunctions[i] = focusFunction; + + return { name, image, recentMessage, i }; + }); + ipcRenderer.send("set-recent-conversations", data); +} + +export function createRecentThreadObserver(): MutationObserver { + const observer = new MutationObserver(recentThreadObserver); + observer.observe( + (document.body.querySelector( + "mws-conversations-list" + ) as unknown) as Element, + { + attributes: false, + subtree: true, + childList: true, + } + ); + return observer; +} diff --git a/src/helpers/settings.ts b/src/helpers/settings.ts index d19012bd..eb70b194 100644 --- a/src/helpers/settings.ts +++ b/src/helpers/settings.ts @@ -39,7 +39,6 @@ export interface JsonSettings { startInTrayEnabled: boolean; autoHideMenuEnabled: boolean; seenMinimizeToTrayWarning: boolean; - seenResetSettingsWarning: boolean; savedWindowSize: WindowSize; savedWindowPosition: WindowPosition | null; checkForUpdateOnLaunchEnabled: boolean; @@ -70,7 +69,6 @@ const defaultSettings: JsonSettings = { startInTrayEnabled: false, autoHideMenuEnabled: false, seenMinimizeToTrayWarning: false, - seenResetSettingsWarning: false, savedWindowSize: { width: 1100, height: 800 }, savedWindowPosition: null, checkForUpdateOnLaunchEnabled: true, diff --git a/src/helpers/trayManager.ts b/src/helpers/trayManager.ts index 471de1bb..5d40f0fa 100644 --- a/src/helpers/trayManager.ts +++ b/src/helpers/trayManager.ts @@ -1,5 +1,6 @@ import { app, + BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, @@ -18,26 +19,27 @@ import { import { settings } from "./settings"; import { v5 as uuidv5 } from "uuid"; import { separator } from "../menu/items/separator"; +import { getMainWindow } from "./getMainWindow"; // bring the settings into scoped const { trayEnabled, - startInTrayEnabled, seenMinimizeToTrayWarning, monochromeIconEnabled, showIconsInRecentConversationTrayEnabled, } = settings; -interface Conversation { +export interface Conversation { name: string | null | undefined; image: string | undefined; recentMessage: string | null | undefined; - click: () => void; + i: number; } export class TrayManager { public enabled = trayEnabled.value; private messagesAreUnread = false; + private recentConversations: Conversation[] = []; public tray: Tray | null = null; @@ -83,8 +85,13 @@ export class TrayManager { } public setRecentConversations(data: Conversation[]): void { - const conversationMenuItems: MenuItemConstructorOptions[] = data.map( - ({ name, click, image, recentMessage }) => { + this.recentConversations = data; + this.refreshTrayMenu(); + } + + public refreshTrayMenu() { + const conversationMenuItems: MenuItemConstructorOptions[] = this.recentConversations.map( + ({ name, image, recentMessage, i }) => { const icon = image != null && image != INITIAL_ICON_IMAGE && @@ -97,10 +104,8 @@ export class TrayManager { sublabel: recentMessage || undefined, icon, click: () => { - if (!app.mainWindow?.isVisible()) { - app.mainWindow?.show(); - } - click(); + getMainWindow()?.show(); + getMainWindow()?.webContents.send("focus-conversation", i); }, }; } @@ -140,7 +145,8 @@ export class TrayManager { } private handleTrayClick() { - app.mainWindow?.show(); + const mainWindow = getMainWindow(); + mainWindow?.show(); } private destroy(): void { @@ -172,10 +178,12 @@ export class TrayManager { if (newValue) { this.startIfEnabled(); + this.refreshTrayMenu(); } else { this.destroy(); - if (!app.mainWindow?.isVisible()) { - app.mainWindow?.show(); + const mainWindow = getMainWindow(); + if (!mainWindow?.isVisible()) { + mainWindow?.show(); } } diff --git a/src/helpers/window.ts b/src/helpers/window.ts deleted file mode 100644 index 72d74b50..00000000 --- a/src/helpers/window.ts +++ /dev/null @@ -1,145 +0,0 @@ -// This helper remembers the size and position of your windows (and restores -// them in that place after app relaunch). -// Can be used for more than one window, just construct many -// instances of it and give each different name. - -import { - app, - BrowserWindow, - BrowserWindowConstructorOptions, - screen, -} from "electron"; -import jetpack from "fs-jetpack"; - -interface Size { - width: number; - height: number; -} - -interface Position extends Size { - x: number; - y: number; -} -/** - * Custom window class that has some utility methods. - * Seems largely uneeded but the code here before used these methods and needed structure. - * This is an improvement over the previous confusion. - * - * @export - * @class Window - * @extends {BrowserWindow} - */ -export class CustomBrowserWindow extends BrowserWindow { - private userDataDir = jetpack.cwd(app.getPath("userData")); - - private stateStoreFile: string; - private defaultSize: Position; - private state: Size; - - constructor(name: string, options?: BrowserWindowConstructorOptions) { - super(options); - this.stateStoreFile = `window-state-${name}.json`; - this.defaultSize = { - width: options?.width || 800, - height: options?.height || 800, - x: 0, - y: 0, - }; - this.resetToDefaults(); - this.state = this.ensureVisibleOnSomeDisplay(); - this.on("close", () => this.saveState()); - } - - /** - * Loads the Position data from cold store. - * - * @returns {Position} - * @memberof Window - */ - public restore(): Position { - let restoredState = {}; - try { - restoredState = this.userDataDir.read(this.stateStoreFile, "json"); - } catch (_err) { - // For some reason json can't be read (might be corrupted). - // No worries, we have defaults. - } - this.defaultSize = { ...this.defaultSize, ...restoredState }; - return this.defaultSize; - } - - /** - * Gets the current window state - * - * @returns {Position} current window state - * @memberof Window - */ - public getCurrentPosition(): Position { - const position = this.getPosition(); - const size = this.getSize(); - return { - x: position[0], - y: position[1], - width: size[0], - height: size[1], - }; - } - - /** - * Checks if the window overlaps with the provided bounds - * - * @param {Position} bounds bounds to check if overlapping - * @returns {boolean} if it overlapped - * @memberof Window - */ - public windowWithinBounds(bounds: Position): boolean { - const windowState = this.getCurrentPosition(); - return ( - windowState.x >= bounds.x && - windowState.y >= bounds.y && - windowState.x + windowState.width <= bounds.x + bounds.width && - windowState.y + windowState.height <= bounds.y + bounds.height - ); - } - - /** - * Resets the default state back to the default - * - * @returns {Position} returns the default Position. - * @memberof Window - */ - public resetToDefaults = (): Position => { - const bounds = screen.getPrimaryDisplay().bounds; - this.defaultSize = { - ...this.defaultSize, - x: (bounds.width - this.defaultSize.width) / 2, - y: (bounds.height - this.defaultSize.height) / 2, - }; - return this.defaultSize; - }; - - /** - * Makes sure the window appears on the display and if not forces it back to the default size - * - * @returns {Position} Returns current window position - * @memberof Window - */ - public ensureVisibleOnSomeDisplay(): Position { - const visible = screen.getAllDisplays().some((display) => { - return this.windowWithinBounds(display.bounds); - }); - if (!visible) { - // Window is partially or fully not visible now. - // Reset it to safe defaults. - return this.resetToDefaults(); - } - return this.getCurrentPosition(); - } - - public saveState(): void { - if (this.isMinimized() && !this.isMaximized()) { - this.state = { ...this.state, ...this.getCurrentPosition() }; - } - this.userDataDir.write(this.stateStoreFile, this.state, { atomic: true }); - } -} diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index 1eb4dc38..00000000 --- a/src/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CustomBrowserWindow } from "./helpers/window"; -import { TrayManager } from "./helpers/trayManager"; -import { Settings as AppSettings } from "./helpers/settings"; - -declare global { - interface Window { - getUserImg: (name: string) => Promise; - } - - namespace Electron { - interface App { - mainWindow?: CustomBrowserWindow; - trayManager?: TrayManager; - settings?: AppSettings; - } - } -} diff --git a/src/menu/contextMenu.ts b/src/menu/contextMenu.ts index 6ee0c1d8..f46e65ae 100644 --- a/src/menu/contextMenu.ts +++ b/src/menu/contextMenu.ts @@ -4,6 +4,7 @@ import { MenuItemConstructorOptions, app, } from "electron"; +import { getMainWindow } from "../helpers/getMainWindow"; import { separator } from "./items/separator"; // WARNING THIS IS THE ONLY PLACE LEFT WITH FORCE TYPECASTS TO ANY @@ -64,7 +65,7 @@ export const popupContextMenu = ( { label: "Add to Dictionary", click: () => - app.mainWindow?.webContents.session.addWordToSpellCheckerDictionary( + getMainWindow()?.webContents.session.addWordToSpellCheckerDictionary( params.misspelledWord ), }, @@ -74,7 +75,7 @@ export const popupContextMenu = ( textMenuTemplateCopy.unshift({ label: suggestion, click: () => - app.mainWindow?.webContents.replaceMisspelling(suggestion), + getMainWindow()?.webContents.replaceMisspelling(suggestion), }); } } diff --git a/src/menu/devMenu.ts b/src/menu/devMenu.ts index 09076cbc..b8279ea7 100644 --- a/src/menu/devMenu.ts +++ b/src/menu/devMenu.ts @@ -1,4 +1,5 @@ import { app, MenuItemConstructorOptions } from "electron"; +import { getMainWindow } from "../helpers/getMainWindow"; export const devMenuTemplate: MenuItemConstructorOptions = { label: "&Development", @@ -6,12 +7,12 @@ export const devMenuTemplate: MenuItemConstructorOptions = { { label: "Reload", accelerator: "CmdOrCtrl+R", - click: (): void => app.mainWindow?.webContents.reloadIgnoringCache(), + click: (): void => getMainWindow()?.webContents.reloadIgnoringCache(), }, { label: "Development Tools", accelerator: "CmdOrCtrl+Shift+I", - click: (): void => app.mainWindow?.webContents.toggleDevTools(), + click: (): void => getMainWindow()?.webContents.toggleDevTools(), }, { label: "Quit", diff --git a/src/menu/items/about.ts b/src/menu/items/about.ts index c95aaf23..8098d29f 100644 --- a/src/menu/items/about.ts +++ b/src/menu/items/about.ts @@ -26,7 +26,7 @@ export const aboutMenuItem: MenuItemConstructorOptions = { copyright: `
Copyright (c) 2020 Kyle Rosenberg${disclaimerText}${licenseText}
`, product_name: productName, description: descriptionWithLocale, - open_devtools: IS_DEV, + open_devtools: false, use_inner_html: true, win_options: { height: 500, diff --git a/src/menu/trayMenu.ts b/src/menu/trayMenu.ts index 059872dd..f87b237d 100644 --- a/src/menu/trayMenu.ts +++ b/src/menu/trayMenu.ts @@ -1,12 +1,13 @@ -import { app, MenuItemConstructorOptions } from "electron"; +import { app, BrowserWindow, MenuItemConstructorOptions } from "electron"; import { IS_MAC } from "../helpers/constants"; +import { getMainWindow } from "../helpers/getMainWindow"; import { separator } from "./items/separator"; export const trayMenuTemplate: MenuItemConstructorOptions[] = [ { label: "Show/Hide Android Messages", click: (): void => { - const mainWindow = app.mainWindow; + const mainWindow = getMainWindow(); if (mainWindow != null) { if (mainWindow.isVisible()) { if (IS_MAC) {