feat(tray): Update to new set of icons and behavior for tray (menubar)
Summary: - See #1698 Add specs Test Plan: - Unit tests Reviewers: bengotow, evan, drew Reviewed By: drew Differential Revision: https://phab.nylas.com/D2734
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
BIN
internal_packages/system-tray/assets/MenuItem-Inbox-Full@1x.png
Normal file
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
BIN
internal_packages/system-tray/assets/MenuItem-Inbox-Zero@1x.png
Normal file
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 21 KiB |
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|