diff --git a/internal_packages/system-tray/assets/darwin/ic-systemtray-nylas.png b/internal_packages/system-tray/assets/MenuItem-Inbox-Full-NewItems@1x.png similarity index 72% rename from internal_packages/system-tray/assets/darwin/ic-systemtray-nylas.png rename to internal_packages/system-tray/assets/MenuItem-Inbox-Full-NewItems@1x.png index 30204c279..fe7fc2cd5 100644 Binary files a/internal_packages/system-tray/assets/darwin/ic-systemtray-nylas.png and b/internal_packages/system-tray/assets/MenuItem-Inbox-Full-NewItems@1x.png differ diff --git a/internal_packages/system-tray/assets/linux/ic-systemtray-nylas.png b/internal_packages/system-tray/assets/MenuItem-Inbox-Full-NewItems@2x.png similarity index 70% rename from internal_packages/system-tray/assets/linux/ic-systemtray-nylas.png rename to internal_packages/system-tray/assets/MenuItem-Inbox-Full-NewItems@2x.png index 9d73b5f2d..ef3f094a3 100644 Binary files a/internal_packages/system-tray/assets/linux/ic-systemtray-nylas.png and b/internal_packages/system-tray/assets/MenuItem-Inbox-Full-NewItems@2x.png differ diff --git a/internal_packages/system-tray/assets/MenuItem-Inbox-Full@1x.png b/internal_packages/system-tray/assets/MenuItem-Inbox-Full@1x.png new file mode 100644 index 000000000..b0c649a07 Binary files /dev/null and b/internal_packages/system-tray/assets/MenuItem-Inbox-Full@1x.png differ diff --git a/internal_packages/system-tray/assets/linux/ic-systemtray-nylas-unread.png b/internal_packages/system-tray/assets/MenuItem-Inbox-Full@2x.png similarity index 70% rename from internal_packages/system-tray/assets/linux/ic-systemtray-nylas-unread.png rename to internal_packages/system-tray/assets/MenuItem-Inbox-Full@2x.png index 53efe1f6e..763fa718a 100644 Binary files a/internal_packages/system-tray/assets/linux/ic-systemtray-nylas-unread.png and b/internal_packages/system-tray/assets/MenuItem-Inbox-Full@2x.png differ diff --git a/internal_packages/system-tray/assets/MenuItem-Inbox-Zero@1x.png b/internal_packages/system-tray/assets/MenuItem-Inbox-Zero@1x.png new file mode 100644 index 000000000..05dfc6a76 Binary files /dev/null and b/internal_packages/system-tray/assets/MenuItem-Inbox-Zero@1x.png differ diff --git a/internal_packages/system-tray/assets/win32/ic-systemtray-nylas.png b/internal_packages/system-tray/assets/MenuItem-Inbox-Zero@2x.png similarity index 65% rename from internal_packages/system-tray/assets/win32/ic-systemtray-nylas.png rename to internal_packages/system-tray/assets/MenuItem-Inbox-Zero@2x.png index 725b50036..0099752c6 100644 Binary files a/internal_packages/system-tray/assets/win32/ic-systemtray-nylas.png and b/internal_packages/system-tray/assets/MenuItem-Inbox-Zero@2x.png differ diff --git a/internal_packages/system-tray/assets/win32/ic-systemtray-nylas-unread.png b/internal_packages/system-tray/assets/win32/ic-systemtray-nylas-unread.png deleted file mode 100644 index d8511905b..000000000 Binary files a/internal_packages/system-tray/assets/win32/ic-systemtray-nylas-unread.png and /dev/null differ diff --git a/internal_packages/system-tray/lib/main.es6 b/internal_packages/system-tray/lib/main.es6 index fb34f42b8..04a5c0724 100644 --- a/internal_packages/system-tray/lib/main.es6 +++ b/internal_packages/system-tray/lib/main.es6 @@ -1,8 +1,7 @@ import SystemTrayIconStore from './system-tray-icon-store'; -const platform = process.platform; export function activate() { - this.store = new SystemTrayIconStore(platform); + this.store = new SystemTrayIconStore(); this.store.activate(); } diff --git a/internal_packages/system-tray/lib/system-tray-icon-store.es6 b/internal_packages/system-tray/lib/system-tray-icon-store.es6 index 5290b6916..5f23da9a9 100644 --- a/internal_packages/system-tray/lib/system-tray-icon-store.es6 +++ b/internal_packages/system-tray/lib/system-tray-icon-store.es6 @@ -1,86 +1,67 @@ -import fs from 'fs'; import path from 'path'; -import mkdirp from 'mkdirp'; -import {remote, ipcRenderer} from 'electron'; -import {UnreadBadgeStore, CanvasUtils} from 'nylas-exports'; -const {canvasWithSystemTrayIconAndText} = CanvasUtils; -const {nativeImage} = remote -const mkdirpAsync = Promise.promisify(mkdirp) -const writeFile = Promise.promisify(fs.writeFile) +import {ipcRenderer} from 'electron'; +import {UnreadBadgeStore} from 'nylas-exports'; // Must be absolute real system path // https://github.com/atom/electron/issues/1299 -const BASE_ICON_PATH = path.join(__dirname, '..', 'assets', process.platform, 'ic-systemtray-nylas.png'); -const UNREAD_ICON_PATH = path.join(__dirname, '..', 'assets', process.platform, 'ic-systemtray-nylas-unread.png'); -const TRAY_ICON_PATH = path.join( - NylasEnv.getConfigDirPath(), - 'tray', - 'tray-icon.png' -); +const INBOX_ZERO_ICON = path.join(__dirname, '..', 'assets', 'MenuItem-Inbox-Zero.png'); +const INBOX_UNREAD_ICON = path.join(__dirname, '..', 'assets', 'MenuItem-Inbox-Full.png'); +const INBOX_UNREAD_ALT_ICON = path.join(__dirname, '..', 'assets', 'MenuItem-Inbox-Full-NewItems.png'); class SystemTrayIconStore { - constructor(platform) { - this._platform = platform; + static INBOX_ZERO_ICON = INBOX_ZERO_ICON; - this._unreadString = (+UnreadBadgeStore.count()).toLocaleString(); - this._unreadIcon = nativeImage.createFromPath(UNREAD_ICON_PATH); - this._baseIcon = nativeImage.createFromPath(BASE_ICON_PATH); - this._icon = this._getIconImg(); + static INBOX_UNREAD_ICON = INBOX_UNREAD_ICON; + + static INBOX_UNREAD_ALT_ICON = INBOX_UNREAD_ALT_ICON; + + constructor() { + this._windowBlurred = false; + this._unsubscribers = []; } activate() { - const iconDir = path.dirname(TRAY_ICON_PATH); - mkdirpAsync(iconDir).then(()=> { - writeFile(TRAY_ICON_PATH, this._icon.toPng()) - .then(()=> { - ipcRenderer.send('update-system-tray', TRAY_ICON_PATH, this._unreadString); - this._unsubscribe = UnreadBadgeStore.listen(this._onUnreadCountChanged); - }) - }); + this._updateIcon() + this._unsubscribers.push(UnreadBadgeStore.listen(this._updateIcon)); + + ipcRenderer.on('browser-window-blur', this._onWindowBlur) + ipcRenderer.on('browser-window-focus', this._onWindowFocus) + this._unsubscribers.push(() => ipcRenderer.removeListener('browser-window-blur', this._onWindowBlur)) + this._unsubscribers.push(() => ipcRenderer.removeListener('browser-window-focus', this._onWindowFocus)) } - _getIconImg(unreadString = this._unreadString) { - const imgHandlers = { - 'darwin': () => { - const img = new Image(); - let canvas = null; - - // toDataUrl always returns the @1x image data, so the assets/darwin/ - // contains an "@2x" image /without/ the @2x extension - img.src = this._baseIcon.toDataURL(); - - if (unreadString === '0') { - canvas = canvasWithSystemTrayIconAndText(img, ''); - } else { - canvas = canvasWithSystemTrayIconAndText(img, unreadString); - } - const pngData = nativeImage.createFromDataURL(canvas.toDataURL()).toPng(); - - // creating from a buffer allows us to specify that the image is @2x - const outputImg = nativeImage.createFromBuffer(pngData); - return outputImg; - }, - 'default': () => { - return unreadString !== '0' ? this._unreadIcon : this._baseIcon; - }, - }; - - return imgHandlers[this._platform in imgHandlers ? this._platform : 'default'](); + _getIconImageData(unreadCount, isWindowBlurred) { + if (unreadCount === 0) { + return {iconPath: INBOX_ZERO_ICON, isTemplateImg: true}; + } + return isWindowBlurred ? + {iconPath: INBOX_UNREAD_ALT_ICON, isTemplateImg: false} : + {iconPath: INBOX_UNREAD_ICON, isTemplateImg: true}; } - _onUnreadCountChanged = () => { - this._unreadString = (+UnreadBadgeStore.count()).toLocaleString(); - this._icon = this._getIconImg(); - writeFile(TRAY_ICON_PATH, this._icon.toPng()) - .then(() => { - ipcRenderer.send('update-system-tray', TRAY_ICON_PATH, this._unreadString); - }); + _onWindowBlur = ()=> { + // Set state to blurred, but don't trigger a change. The icon should only be + // updated when the count changes + this._windowBlurred = true; + }; + + _onWindowFocus = ()=> { + // Make sure that as long as the window is focused we never use the alt icon + this._windowBlurred = false; + this._updateIcon(); + }; + + _updateIcon = () => { + const count = UnreadBadgeStore.count() + const unreadString = (+count).toLocaleString(); + const {iconPath, isTemplateImg} = this._getIconImageData(count, this._windowBlurred); + ipcRenderer.send('update-system-tray', iconPath, unreadString, isTemplateImg); }; deactivate() { - if (this._unsubscribe) this._unsubscribe(); + this._unsubscribers.forEach(unsub => unsub()) } } diff --git a/internal_packages/system-tray/spec/system-tray-icon-store-spec.es6 b/internal_packages/system-tray/spec/system-tray-icon-store-spec.es6 new file mode 100644 index 000000000..262cf6cc4 --- /dev/null +++ b/internal_packages/system-tray/spec/system-tray-icon-store-spec.es6 @@ -0,0 +1,81 @@ +import {ipcRenderer} from 'electron'; +import {UnreadBadgeStore} from 'nylas-exports'; +import SystemTrayIconStore from '../lib/system-tray-icon-store'; + +const { + INBOX_ZERO_ICON, + INBOX_UNREAD_ICON, + INBOX_UNREAD_ALT_ICON, +} = SystemTrayIconStore; + + +describe('SystemTrayIconStore', ()=> { + beforeEach(()=> { + spyOn(ipcRenderer, 'send') + this.iconStore = new SystemTrayIconStore() + }); + + function getCallData() { + const {args} = ipcRenderer.send.calls[0] + return {iconPath: args[1], isTemplateImg: args[3]} + } + + describe('_getIconImageData', ()=> { + it('shows inbox zero icon when unread count is 0 and window is focused', ()=> { + const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(0, false) + expect(iconPath).toBe(INBOX_ZERO_ICON) + expect(isTemplateImg).toBe(true) + }); + + it('shows inbox zero icon when unread count is 0 and window is blurred', ()=> { + const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(0, true) + expect(iconPath).toBe(INBOX_ZERO_ICON) + expect(isTemplateImg).toBe(true) + }); + + it('shows inbox full icon when unread count > 0 and window is focused', ()=> { + const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(1, false) + expect(iconPath).toBe(INBOX_UNREAD_ICON) + expect(isTemplateImg).toBe(true) + }); + + it('shows inbox full /alt/ icon when unread count > 0 and window is blurred', ()=> { + const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(1, true) + expect(iconPath).toBe(INBOX_UNREAD_ALT_ICON) + expect(isTemplateImg).toBe(false) + }); + }); + + describe('updating the icon based on focus and blur', ()=> { + it('always shows inbox full icon when the window gets focused', ()=> { + spyOn(UnreadBadgeStore, 'count').andReturn(1) + this.iconStore._onWindowFocus() + const {iconPath} = getCallData() + expect(iconPath).toBe(INBOX_UNREAD_ICON) + }); + + it('shows inbox full /alt/ icon ONLY when window is currently blurred and unread count changes', ()=> { + this.iconStore._windowBlurred = false + this.iconStore._onWindowBlur() + expect(ipcRenderer.send).not.toHaveBeenCalled() + + // UnreadBadgeStore triggers a change + spyOn(UnreadBadgeStore, 'count').andReturn(1) + this.iconStore._updateIcon() + + const {iconPath} = getCallData() + expect(iconPath).toBe(INBOX_UNREAD_ALT_ICON) + }); + + it('does not show inbox full /alt/ icon when window is currently focused and unread count changes', ()=> { + this.iconStore._windowBlurred = false + + // UnreadBadgeStore triggers a change + spyOn(UnreadBadgeStore, 'count').andReturn(1) + this.iconStore._updateIcon() + + const {iconPath} = getCallData() + expect(iconPath).toBe(INBOX_UNREAD_ICON) + }); + }); +}); diff --git a/src/browser/application.coffee b/src/browser/application.coffee index 4dd49189d..e8b999d25 100644 --- a/src/browser/application.coffee +++ b/src/browser/application.coffee @@ -347,8 +347,8 @@ class Application event.preventDefault() # System Tray - ipcMain.on 'update-system-tray', (event, iconPath, unreadString) => - @systemTrayManager?.setTrayCount(iconPath, unreadString) + ipcMain.on 'update-system-tray', (event, args...) => + @systemTrayManager?.updateTray(args...) ipcMain.on 'set-badge-value', (event, value) => app.dock?.setBadge?(value) diff --git a/src/browser/system-tray-manager.es6 b/src/browser/system-tray-manager.es6 index 4700a2eef..5f0126305 100644 --- a/src/browser/system-tray-manager.es6 +++ b/src/browser/system-tray-manager.es6 @@ -1,4 +1,3 @@ -import fs from 'fs'; import {Tray, Menu, nativeImage} from 'electron'; @@ -35,20 +34,15 @@ function _getTooltip(unreadString) { return unreadString ? '' : `${unreadString} unread messages`; } -function _getIcon(iconPath) { - if (!iconPath) return nativeImage.createEmpty() - try { - fs.accessSync(iconPath, fs.F_OK | fs.R_OK) - const buffer = fs.readFileSync(iconPath); - if (buffer.length > 0) { - const out2x = nativeImage.createFromBuffer(buffer, 2); - out2x.setTemplateImage(true); - return out2x; - } - return nativeImage.createEmpty(); - } catch (e) { +function _getIcon(iconPath, isTemplateImg) { + if (!iconPath) { return nativeImage.createEmpty(); } + const icon = nativeImage.createFromPath(iconPath) + if (isTemplateImg) { + icon.setTemplateImage(true); + } + return icon; } @@ -83,20 +77,19 @@ class SystemTrayManager { } }; - setTrayCount(iconPath, unreadString) { + updateTray(iconPath, unreadString, isTemplateImg) { if (!this._tray) return; this._iconPath = iconPath; - const icon = _getIcon(this._iconPath); + const icon = _getIcon(this._iconPath, isTemplateImg); const tooltip = _getTooltip(unreadString); this._tray.setImage(icon); this._tray.setToolTip(tooltip); } destroy() { - if (!this._tray) return; + if (this._tray) this._tray.destroy(); this._unsubscribe(); - this._tray.destroy(); } }