diff --git a/app/main/autoupdater.js b/app/main/autoupdater.js index 6ad85e55..3bd7d761 100644 --- a/app/main/autoupdater.js +++ b/app/main/autoupdater.js @@ -8,7 +8,7 @@ function appUpdater() { log.transports.file.level = 'info'; autoUpdater.logger = log; /* - autoUpdater.on('error', err => log.info(err)); + AutoUpdater.on('error', err => log.info(err)); autoUpdater.on('checking-for-update', () => log.info('checking-for-update')); autoUpdater.on('update-available', () => log.info('update-available')); autoUpdater.on('update-not-available', () => log.info('update-not-available')); @@ -17,7 +17,7 @@ function appUpdater() { // Ask the user if update is available // eslint-disable-next-line no-unused-vars autoUpdater.on('update-downloaded', (event, info) => { - // let message = app.getName() + ' ' + info.releaseName + ' is now available. It will be installed the next time you restart the application.'; + // Let message = app.getName() + ' ' + info.releaseName + ' is now available. It will be installed the next time you restart the application.'; // if (info.releaseNotes) { // const splitNotes = info.releaseNotes.split(/[^\r]\n/); // message += '\n\nRelease notes:\n'; @@ -38,7 +38,7 @@ function appUpdater() { } }); }); - // init for updates + // Init for updates autoUpdater.checkForUpdates(); } diff --git a/app/main/index.js b/app/main/index.js index 0cde8d86..5264d89f 100644 --- a/app/main/index.js +++ b/app/main/index.js @@ -12,7 +12,7 @@ const electronLocalshortcut = require('electron-localshortcut'); const Configstore = require('electron-config'); const JsonDB = require('node-json-db'); const isDev = require('electron-is-dev'); -const tray = require('./tray'); +// Not using now //const tray = require('./tray'); const appMenu = require('./menu'); const {linkIsInternal, skipImages} = require('./link-helper'); const {appUpdater} = require('./autoupdater'); @@ -20,7 +20,7 @@ const {appUpdater} = require('./autoupdater'); const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true); const data = db.getData('/'); -// adds debug features like hotkeys for triggering dev tools and reload +// Adds debug features like hotkeys for triggering dev tools and reload require('electron-debug')(); const conf = new Configstore(); @@ -41,10 +41,10 @@ function userOS() { } } -// setting userAgent so that server-side code can identify the desktop app +// Setting userAgent so that server-side code can identify the desktop app const isUserAgent = 'ZulipElectron/' + app.getVersion() + ' ' + userOS(); -// prevent window being garbage collected +// Prevent window being garbage collected let mainWindow; let targetLink; @@ -93,6 +93,7 @@ function checkConnectivity() { }, index => { if (index === 0) { mainWindow.webContents.reload(); + mainWindow.webContents.send('destroytray'); } if (index === 1) { app.quit(); @@ -158,6 +159,7 @@ function updateDockBadge(title) { if (process.platform === 'darwin') { app.setBadgeCount(messageCount); } + mainWindow.webContents.send('tray', messageCount); } function createMainWindow() { @@ -217,7 +219,7 @@ function createMainWindow() { }); }); - // on osx it's 'moved' + // On osx it's 'moved' win.on('move', function () { const pos = this.getPosition(); conf.set({ @@ -226,12 +228,19 @@ function createMainWindow() { }); }); - // stop page to update it's title + // Stop page to update it's title win.on('page-title-updated', (e, title) => { e.preventDefault(); updateDockBadge(title); }); + // To destroy tray icon when navigate to a new URL + win.webContents.on('will-navigate', e => { + if (e) { + win.webContents.send('destroytray'); + } + }); + return win; } @@ -239,7 +248,7 @@ function createMainWindow() { app.commandLine.appendSwitch('ignore-certificate-errors', 'true'); app.on('window-all-closed', () => { - // unregister all the shortcuts so that they don't interfare with other apps + // Unregister all the shortcuts so that they don't interfare with other apps electronLocalshortcut.unregisterAll(mainWindow); if (process.platform !== 'darwin') { app.quit(); @@ -255,13 +264,14 @@ app.on('activate', () => { app.on('ready', () => { electron.Menu.setApplicationMenu(appMenu); mainWindow = createMainWindow(); - tray.create(); + // Not using for now // tray.create(); const page = mainWindow.webContents; // TODO - use global shortcut instead electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => { mainWindow.reload(); + mainWindow.webContents.send('destroytray'); }); electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => { @@ -301,19 +311,22 @@ app.on('ready', () => { }); app.on('will-quit', () => { - // unregister all the shortcuts so that they don't interfare with other apps + // Unregister all the shortcuts so that they don't interfare with other apps electronLocalshortcut.unregisterAll(mainWindow); }); ipc.on('new-domain', (e, domain) => { - // mainWindow.loadURL(domain); + // MainWindow.loadURL(domain); if (!mainWindow) { mainWindow = createMainWindow(); mainWindow.loadURL(domain); + mainWindow.webContents.send('destroytray'); } else if (mainWindow.isMinimized()) { + mainWindow.webContents.send('destroytray'); mainWindow.loadURL(domain); mainWindow.show(); } else { + mainWindow.webContents.send('destroytray'); mainWindow.loadURL(domain); serverError(domain); } diff --git a/app/main/menu.js b/app/main/menu.js index cffcb739..ddc81051 100644 --- a/app/main/menu.js +++ b/app/main/menu.js @@ -8,7 +8,7 @@ const app = electron.app; const BrowserWindow = electron.BrowserWindow; const shell = electron.shell; const appName = app.getName(); -const tray = require('./tray'); +// Const tray = require('./tray'); const {addDomain, about} = require('./windowmanager'); @@ -36,6 +36,7 @@ const viewSubmenu = [ click(item, focusedWindow) { if (focusedWindow) { focusedWindow.reload(); + focusedWindow.webContents.send('destroytray'); } } }, @@ -79,7 +80,7 @@ const viewSubmenu = [ label: 'Toggle Tray Icon', click(item, focusedWindow) { if (focusedWindow) { - tray.toggle(); + focusedWindow.webContents.send('toggletray'); } } }, diff --git a/app/main/preload.js b/app/main/preload.js index 26169cc2..ada3dc50 100644 --- a/app/main/preload.js +++ b/app/main/preload.js @@ -12,8 +12,11 @@ process.once('loaded', () => { // eslint-disable-next-line import/no-unassigned-import require('./domain'); +// eslint-disable-next-line import/no-unassigned-import +require('../renderer/js/tray.js'); +// Calling Tray.js in renderer process everytime app window loads -// handle zooming functionality +// Handle zooming functionality const zoomIn = () => { webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1); }; @@ -26,7 +29,7 @@ const zoomActualSize = () => { webFrame.setZoomFactor(1); }; -// get zooming actions from main process +// Get zooming actions from main process ipcRenderer.on('zoomIn', () => { zoomIn(); }); @@ -40,7 +43,7 @@ ipcRenderer.on('zoomActualSize', () => { }); ipcRenderer.on('log-out', () => { - // create the menu for the below + // Create the menu for the below document.querySelector('.dropdown-toggle').click(); const nodes = document.querySelectorAll('.dropdown-menu li:last-child a'); @@ -48,19 +51,19 @@ ipcRenderer.on('log-out', () => { }); ipcRenderer.on('shortcut', () => { - // create the menu for the below + // Create the menu for the below const node = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]'); - // additional check + // Additional check if (node.text.trim().toLowerCase() === 'keyboard shortcuts') { node.click(); } else { - // atleast click the dropdown + // Atleast click the dropdown document.querySelector('.dropdown-toggle').click(); } }); // To prevent failing this script on linux we need to load it after the document loaded document.addEventListener('DOMContentLoaded', () => { - // init spellchecker + // Init spellchecker spellChecker(); }); diff --git a/app/main/windowmanager.js b/app/main/windowmanager.js index b7f551ff..a730aa01 100644 --- a/app/main/windowmanager.js +++ b/app/main/windowmanager.js @@ -1,6 +1,7 @@ 'use strict'; const path = require('path'); const electron = require('electron'); +const ipc = require('electron').ipcMain; const APP_ICON = path.join(__dirname, '../resources', 'Icon'); @@ -11,7 +12,7 @@ let domainWindow; let aboutWindow; function onClosed() { - // dereference the window + // Dereference the window domainWindow = null; aboutWindow = null; } @@ -64,7 +65,7 @@ function createAboutWindow() { aboutwin.loadURL(aboutURL); aboutwin.on('closed', onClosed); - // stop page to update it's title + // Stop page to update it's title aboutwin.on('page-title-updated', e => { e.preventDefault(); }); @@ -82,6 +83,17 @@ function about() { }); } +ipc.on('trayabout', event => { + if (event) { + about(); + } +}); + +ipc.on('traychangeserver', event => { + if (event) { + addDomain(); + } +}); module.exports = { addDomain, about diff --git a/app/renderer/js/tray.js b/app/renderer/js/tray.js new file mode 100644 index 00000000..50311e63 --- /dev/null +++ b/app/renderer/js/tray.js @@ -0,0 +1,188 @@ +'use strict'; +const path = require('path'); + +const electron = require('electron'); + +const {ipcRenderer, remote} = electron; + +const {Tray, Menu, nativeImage} = remote; + +const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray'); + +const iconPath = () => { + if (process.platform === 'linux') { + return APP_ICON + 'linux.png'; + } + return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'osx.png'); +}; + +let unread = 0; + +const trayIconSize = () => { + switch (process.platform) { + case 'darwin': + return 20; + case 'win32': + return 100; + case 'linux': + return 100; + default: return 80; + } +}; + +// Default config for Icon we might make it OS specific if needed like the size +const config = { + pixelRatio: window.devicePixelRatio, + unreadCount: 0, + showUnreadCount: true, + unreadColor: '#000000', + readColor: '#000000', + unreadBackgroundColor: '#B9FEEA', + readBackgroundColor: '#B9FEEA', + size: trayIconSize(), + thick: process.platform === 'win32' +}; + +const renderCanvas = function (arg) { + config.unreadCount = arg; + return new Promise((resolve, reject) => { + const SIZE = config.size * config.pixelRatio; + const PADDING = SIZE * 0.05; + const CENTER = SIZE / 2; + const HAS_COUNT = config.showUnreadCount && config.unreadCount; + const color = config.unreadCount ? config.unreadColor : config.readColor; + const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor; + + const canvas = document.createElement('canvas'); + canvas.width = SIZE; + canvas.height = SIZE; + const ctx = canvas.getContext('2d'); + + // Circle + // If (!config.thick || config.thick && HAS_COUNT) { + ctx.beginPath(); + ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false); + ctx.fillStyle = backgroundColor; + ctx.fill(); + ctx.lineWidth = SIZE / (config.thick ? 10 : 20); + ctx.strokeStyle = backgroundColor; + ctx.stroke(); + // Count or Icon + if (HAS_COUNT) { + ctx.fillStyle = color; + ctx.textAlign = 'center'; + if (config.unreadCount > 99) { + ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`; + ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15)); + } else if (config.unreadCount < 10) { + ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; + ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.20)); + } else { + ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; + ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.15)); + } + + resolve(canvas); + } else { + reject(canvas); + } + }); +}; +/** + * Renders the tray icon as a native image + * @param arg: Unread count + * @return the native image + */ +const renderNativeImage = function (arg) { + return Promise.resolve() + .then(() => renderCanvas(arg)) + .then(canvas => { + const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPng(); + return Promise.resolve(nativeImage.createFromBuffer(pngData, config.pixelRatio)); + }); +}; + +const createTray = function () { + window.tray = new Tray(iconPath()); + const contextMenu = Menu.buildFromTemplate([{ + label: 'About', + click() { + ipcRenderer.send('trayabout'); + } + }, + { + type: 'separator' + }, + { + label: 'Change Zulip server', + click() { + ipcRenderer.send('traychangeserver'); + } + }, + { + type: 'separator' + }, + { + label: 'Reload', + click() { + remote.getCurrentWindow().reload(); + window.tray.destroy(); + } + }, + { + type: 'separator' + }, + { + label: 'Quit', + click() { + remote.getCurrentWindow().close(); + } + } + ]); + window.tray.setContextMenu(contextMenu); +}; + +ipcRenderer.on('destroytray', event => { + window.tray.destroy(); + if (window.tray.isDestroyed()) { + window.tray = null; + } else { + throw new Error('Tray icon not properly destroyed.'); + } + + return event; +}); + +ipcRenderer.on('tray', (event, arg) => { + if (arg === 0) { + unread = arg; + // Message Count // console.log("message count is zero."); + window.tray.setImage(iconPath()); + window.tray.setToolTip('No unread messages'); + } else { + unread = arg; + renderNativeImage(arg).then(image => { + window.tray.setImage(image); + window.tray.setToolTip(arg + ' unread messages'); + }); + } +}); + +ipcRenderer.on('toggletray', event => { + if (event) { + if (window.tray) { + window.tray.destroy(); + if (window.tray.isDestroyed()) { + window.tray = null; + } + } else { + createTray(); + renderNativeImage(unread).then(image => { + window.tray.setImage(image); + window.tray.setToolTip(unread + ' unread messages'); + }); + } + } +}); + +createTray();