From f5fd71a227bf7809a18c59794495ba63c541c8ed Mon Sep 17 00:00:00 2001 From: Chris Knepper Date: Wed, 27 Jun 2018 21:17:16 -0400 Subject: [PATCH 1/3] -Add notification badge on macOS (Fixes #15) -Add Command+H shortcut to hide window on macOS (Fixes #16, Fixes #22) -Add Windows Tray icon support (Fixes #21) --- src/app.js | 44 +++++++++++++++----- src/background.js | 78 +++++++++++++++++++++++++++++++---- src/menu/app_menu_template.js | 8 ++++ 3 files changed, 113 insertions(+), 17 deletions(-) diff --git a/src/app.js b/src/app.js index 04a60ee1..f3479e8e 100644 --- a/src/app.js +++ b/src/app.js @@ -3,13 +3,15 @@ import './stylesheets/main.css'; import './helpers/context_menu.js'; import './helpers/external_links.js'; -const state = { - loaded: false -}; - import { remote, shell } from 'electron'; import url from 'url'; import jetpack from 'fs-jetpack'; +import { IS_MAC } from './constants'; + +const state = { + loaded: false, + unreadNotificationCount: 0 +}; const app = remote.app; const appDir = jetpack.cwd(app.getAppPath()); @@ -17,24 +19,46 @@ const appDir = jetpack.cwd(app.getAppPath()); // TODO: Insert or update webview here instead of in the HTML file to make testing (swapping URLs) easier androidMessagesWebview.addEventListener('did-start-loading', () => { + // Intercept request for notifications and accept it androidMessagesWebview.getWebContents().session.setPermissionRequestHandler((webContents, permission, callback) => { const url = webContents.getURL() - if (permission === 'notifications') { + /* + * We always get a "notification" in dev mode when the app starts due to calling setPermissionRequestHandler, + * which accepts the permission to send browser notifications on behalf of the user--this false + * notification should not result in an indicator for the user to see. + * TODO: Figure out a way to modify and override notifications to solve this and other issues. + */ + if (IS_MAC) { + if (app.mainWindow && !(app.mainWindow.isFocused())) { + state.unreadNotificationCount += 1; + app.dock.setBadge('' + state.unreadNotificationCount); + } + } + + // TODO: Provide visual indicators for Windows/Linux, possibly via mainWindow.setOverlayIcon + return callback(true); // Approve } - // if (!url.startsWith('https://my-website.com')) { - // return callback(false) // Deny - // } + if (!url.startsWith('https://messages.android.com')) { + return callback(false); // Deny + } }); + + if (IS_MAC && app.mainWindow) { + app.mainWindow.on('focus', () => { + state.unreadNotificationCount = 0; + app.dock.setBadge(''); + }) + } }); androidMessagesWebview.addEventListener('did-finish-load', () => { // just before onLoad console.log('finished loading'); - + }); androidMessagesWebview.addEventListener('did-stop-loading', () => { // coincident with onLoad, can fire multiple times @@ -43,7 +67,7 @@ androidMessagesWebview.addEventListener('did-stop-loading', () => { // coinciden state.loaded = true; loader.classList.add('hidden'); } - + }); androidMessagesWebview.addEventListener('dom-ready', () => { diff --git a/src/background.js b/src/background.js index 584d74dc..5d307e2b 100644 --- a/src/background.js +++ b/src/background.js @@ -5,18 +5,20 @@ import path from 'path'; import url from 'url'; -import { app, Menu } from 'electron'; +import { app, Menu, Tray } from 'electron'; import { autoUpdater } from 'electron-updater'; import { baseMenuTemplate } from './menu/base_menu_template'; import { devMenuTemplate } from './menu/dev_menu_template'; import { helpMenuTemplate } from './menu/help_menu_template'; import createWindow from './helpers/window'; -import { IS_WINDOWS } from './constants'; +import { IS_MAC, IS_WINDOWS, IS_LINUX, IS_DEV } from './constants'; // Special module holding environment variables which you declared // in config/env_xxx.json file. import env from 'env'; +let tray; // Must declare reference to instance of Tray as a variable, not a const, or bad/weird things happen + const setApplicationMenu = () => { const menus = baseMenuTemplate; if (env.name !== 'production') { @@ -58,11 +60,73 @@ app.on('ready', () => { }) ); - if (env.name === 'development') { - mainWindow.openDevTools(); + app.mainWindow = mainWindow; // Quick and dirty way for renderer process to access mainWindow for communication + + if (IS_MAC) { + let quitViaContext = false; + app.on('before-quit', () => { + quitViaContext = true; + }); + + mainWindow.on('close', (event) => { + if (!quitViaContext) { + event.preventDefault(); + mainWindow.hide(); + } + }); + + app.on('activate', () => { + mainWindow.show(); + }); } -}); -app.on('window-all-closed', () => { - app.quit(); + if (IS_WINDOWS) { + mainWindow.on('close', (event) => { + app.quit(); + }); + + tray = new Tray(__dirname + '../../resources/icon.ico'); + + let contextMenu = Menu.buildFromTemplate([ + { + label: 'Show', + click: () => { + mainWindow.show(); + } + }, + { + label: 'Quit', + click: () => { + app.quit(); + } + } + ]); + + tray.setContextMenu(contextMenu); + + tray.on('double-click', (event) => { + event.preventDefault(); + mainWindow.show(); + }); + + mainWindow.on('minimize', (event) => { + event.preventDefault(); + // TODO: Hide the window via mainWindow.hide() instead of minimizing? + // Hiding would allow the icon to disappear from the taskbar if it's not pinned, + // but if it's pinned, hidden, then clicked, results in a duplicate instance of the app... + // Possible solution: https://github.com/electron/electron/blob/v0.36.10/docs/api/app.md#appmakesingleinstancecallback + mainWindow.minimize(); + }); + } + + // TODO: Better UX for Linux...likely similar to Windows as far as tray behavior + if (IS_LINUX) { + app.on('window-all-closed', (event) => { + app.quit(); + }); + } + + if (IS_DEV) { + mainWindow.openDevTools(); + } }); diff --git a/src/menu/app_menu_template.js b/src/menu/app_menu_template.js index 7cd2b8d7..5d5a6e30 100644 --- a/src/menu/app_menu_template.js +++ b/src/menu/app_menu_template.js @@ -12,6 +12,14 @@ export const appMenuTemplate = { { type: 'separator', }, + { + label: 'Hide Android Messages Desktop', + accelerator: 'Command+H', + click: () => app.mainWindow && app.mainWindow.hide() + }, + { + type: 'separator', + }, { label: 'Quit', accelerator: 'Command+Q', From 166ebe04e6aa78d04ba595e3706044f45bd2c516 Mon Sep 17 00:00:00 2001 From: Chris Knepper Date: Wed, 27 Jun 2018 22:14:27 -0400 Subject: [PATCH 2/3] -Prevent multiple instances of the app launching which was a potentially large problem on Windows -True minimizing "to" the Windows Tray --- src/background.js | 218 +++++++++++++++++++++++++--------------------- 1 file changed, 118 insertions(+), 100 deletions(-) diff --git a/src/background.js b/src/background.js index 5d307e2b..56eac3ee 100644 --- a/src/background.js +++ b/src/background.js @@ -17,116 +17,134 @@ import { IS_MAC, IS_WINDOWS, IS_LINUX, IS_DEV } from './constants'; // in config/env_xxx.json file. import env from 'env'; -let tray; // Must declare reference to instance of Tray as a variable, not a const, or bad/weird things happen +let mainWindow = null; + +// Prevent multiple instances of the app which causes many problems with an app like ours +// Without this, if an instance were minimized to the tray in Windows, clicking a shortcut would launch another instance, icky +// Adapted from https://github.com/electron/electron/blob/v2.0.2/docs/api/app.md#appmakesingleinstancecallback +const isSecondInstance = app.makeSingleInstance((commandLine, workingDirectory) => { + // Someone tried to run a second instance, let's show our existing instance instead + if (mainWindow) { + if (!mainWindow.isVisible()) { + mainWindow.show(); + } + } +}); -const setApplicationMenu = () => { - const menus = baseMenuTemplate; +if (isSecondInstance) { + app.quit() +} else { + let tray; // Must declare reference to instance of Tray as a variable, not a const, or bad/weird things happen + + const setApplicationMenu = () => { + const menus = baseMenuTemplate; + if (env.name !== 'production') { + 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 (env.name !== 'production') { - menus.push(devMenuTemplate); + const userDataPath = app.getPath('userData'); + app.setPath('userData', `${userDataPath} (${env.name})`); } - 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 (env.name !== 'production') { - const userDataPath = app.getPath('userData'); - app.setPath('userData', `${userDataPath} (${env.name})`); -} - -if (IS_WINDOWS) { - // Stupid, DUMB calls that have to be made to let notifications come through on Windows (only Windows 10?) - // See: https://github.com/electron/electron/issues/10864#issuecomment-382519150 - app.setAppUserModelId('com.knepper.android-messages-desktop'); - app.setAsDefaultProtocolClient('android-messages-desktop'); -} - -app.on('ready', () => { - setApplicationMenu(); - autoUpdater.checkForUpdatesAndNotify(); - - const mainWindow = createWindow('main', { - width: 1100, - height: 800 - }); - - mainWindow.loadURL( - url.format({ - pathname: path.join(__dirname, 'app.html'), - protocol: 'file:', - slashes: true - }) - ); - app.mainWindow = mainWindow; // Quick and dirty way for renderer process to access mainWindow for communication - - if (IS_MAC) { - let quitViaContext = false; - app.on('before-quit', () => { - quitViaContext = true; - }); - - mainWindow.on('close', (event) => { - if (!quitViaContext) { - event.preventDefault(); - mainWindow.hide(); - } - }); - - app.on('activate', () => { - mainWindow.show(); - }); + if (IS_WINDOWS) { + // Stupid, DUMB calls that have to be made to let notifications come through on Windows (only Windows 10?) + // See: https://github.com/electron/electron/issues/10864#issuecomment-382519150 + app.setAppUserModelId('com.knepper.android-messages-desktop'); + app.setAsDefaultProtocolClient('android-messages-desktop'); } - if (IS_WINDOWS) { - mainWindow.on('close', (event) => { - app.quit(); - }); + app.on('ready', () => { + setApplicationMenu(); + autoUpdater.checkForUpdatesAndNotify(); - tray = new Tray(__dirname + '../../resources/icon.ico'); + mainWindow = createWindow('main', { + width: 1100, + height: 800 + }); - let contextMenu = Menu.buildFromTemplate([ - { - label: 'Show', - click: () => { - mainWindow.show(); + mainWindow.loadURL( + url.format({ + pathname: path.join(__dirname, 'app.html'), + protocol: 'file:', + slashes: true + }) + ); + + app.mainWindow = mainWindow; // Quick and dirty way for renderer process to access mainWindow for communication + + if (IS_MAC) { + let quitViaContext = false; + app.on('before-quit', () => { + quitViaContext = true; + }); + + mainWindow.on('close', (event) => { + if (!quitViaContext) { + event.preventDefault(); + mainWindow.hide(); } - }, - { - label: 'Quit', - click: () => { - app.quit(); + }); + + app.on('activate', () => { + mainWindow.show(); + }); + } + + if (IS_WINDOWS) { + mainWindow.on('close', (event) => { + app.quit(); + }); + + tray = new Tray(__dirname + '../../resources/icon.ico'); + + let contextMenu = Menu.buildFromTemplate([ + { + label: 'Show', + click: () => { + mainWindow.show(); + } + }, + { + label: 'Quit', + click: () => { + app.quit(); + } } - } - ]); - - tray.setContextMenu(contextMenu); + ]); - tray.on('double-click', (event) => { - event.preventDefault(); - mainWindow.show(); - }); - - mainWindow.on('minimize', (event) => { - event.preventDefault(); - // TODO: Hide the window via mainWindow.hide() instead of minimizing? - // Hiding would allow the icon to disappear from the taskbar if it's not pinned, - // but if it's pinned, hidden, then clicked, results in a duplicate instance of the app... - // Possible solution: https://github.com/electron/electron/blob/v0.36.10/docs/api/app.md#appmakesingleinstancecallback - mainWindow.minimize(); - }); - } + tray.setContextMenu(contextMenu); - // TODO: Better UX for Linux...likely similar to Windows as far as tray behavior - if (IS_LINUX) { - app.on('window-all-closed', (event) => { - app.quit(); - }); - } + tray.on('double-click', (event) => { + event.preventDefault(); + mainWindow.show(); + }); - if (IS_DEV) { - mainWindow.openDevTools(); - } -}); + mainWindow.on('minimize', (event) => { + event.preventDefault(); + // TODO: Hide the window via mainWindow.hide() instead of minimizing? + // Hiding would allow the icon to disappear from the taskbar if it's not pinned, + // but if it's pinned, hidden, then clicked, results in a duplicate instance of the app... + // Possible solution: https://github.com/electron/electron/blob/v0.36.10/docs/api/app.md#appmakesingleinstancecallback + mainWindow.hide(); + }); + } + + // TODO: Better UX for Linux...likely similar to Windows as far as tray behavior + if (IS_LINUX) { + app.on('window-all-closed', (event) => { + app.quit(); + }); + } + + if (IS_DEV) { + mainWindow.openDevTools(); + } + }); +} From 810dc5bc7e73a7a844854bb8218a40a8ef3dd9f0 Mon Sep 17 00:00:00 2001 From: Chris Knepper Date: Wed, 27 Jun 2018 22:15:31 -0400 Subject: [PATCH 3/3] -Remove extraneous comment --- src/background.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/background.js b/src/background.js index 56eac3ee..d96e4481 100644 --- a/src/background.js +++ b/src/background.js @@ -128,10 +128,6 @@ if (isSecondInstance) { mainWindow.on('minimize', (event) => { event.preventDefault(); - // TODO: Hide the window via mainWindow.hide() instead of minimizing? - // Hiding would allow the icon to disappear from the taskbar if it's not pinned, - // but if it's pinned, hidden, then clicked, results in a duplicate instance of the app... - // Possible solution: https://github.com/electron/electron/blob/v0.36.10/docs/api/app.md#appmakesingleinstancecallback mainWindow.hide(); }); }