Skip to content

Commit

Permalink
Bullet-proof tray icon against nonexistent icon file
Browse files Browse the repository at this point in the history
Co-authored-by: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com>
  • Loading branch information
automated-signal and EvanHahn-Signal committed Oct 7, 2021
1 parent c318a04 commit 54fe694
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 6 deletions.
26 changes: 23 additions & 3 deletions app/SystemTrayService.ts
Expand Up @@ -2,7 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only

import { join } from 'path';
import { BrowserWindow, app, Menu, Tray } from 'electron';
import {
BrowserWindow,
Menu,
NativeImage,
Tray,
app,
nativeImage,
} from 'electron';
import * as log from '../ts/logging/log';
import type { LocaleMessagesType } from '../ts/types/I18N';

Expand Down Expand Up @@ -105,7 +112,14 @@ export class SystemTrayService {
this.tray = this.tray || this.createTray();
const { browserWindow, tray } = this;

tray.setImage(getIcon(this.unreadCount));
try {
tray.setImage(getIcon(this.unreadCount));
} catch (err: unknown) {
log.warn(
'System tray service: failed to set preferred image. Falling back...'
);
tray.setImage(getDefaultIcon());
}

// NOTE: we want to have the show/hide entry available in the tray icon
// context menu, since the 'click' event may not work on all platforms.
Expand Down Expand Up @@ -169,7 +183,7 @@ export class SystemTrayService {
log.info('System tray service: creating the tray');

// This icon may be swiftly overwritten.
const result = new Tray(getIcon(this.unreadCount));
const result = new Tray(getDefaultIcon());

// Note: "When app indicator is used on Linux, the click event is ignored." This
// doesn't mean that the click event is always ignored on Linux; it depends on how
Expand Down Expand Up @@ -223,6 +237,12 @@ function getIcon(unreadCount: number) {
return join(__dirname, '..', 'images', `icon_${iconSize}.png`);
}

let defaultIcon: undefined | NativeImage;
function getDefaultIcon(): NativeImage {
defaultIcon ??= nativeImage.createFromPath(getIcon(0));
return defaultIcon;
}

function forceOnTop(browserWindow: BrowserWindow) {
// On some versions of GNOME the window may not be on top when restored.
// This trick should fix it.
Expand Down
29 changes: 26 additions & 3 deletions ts/test-node/app/SystemTrayService_test.ts
Expand Up @@ -3,7 +3,7 @@

import { assert } from 'chai';
import * as sinon from 'sinon';
import { BrowserWindow, MenuItem, Tray } from 'electron';
import { BrowserWindow, MenuItem, Tray, nativeImage } from 'electron';
import * as path from 'path';

import { SystemTrayService } from '../../../app/SystemTrayService';
Expand Down Expand Up @@ -208,9 +208,9 @@ describe('SystemTrayService', () => {
// Ideally, there'd be something like `tray.getImage`, but that doesn't exist. We also
// can't spy on `Tray.prototype.setImage` because it's not defined that way. So we
// spy on the specific instance, just to get the image.
const setContextMenuSpy = sandbox.spy(tray, 'setImage');
const setImageSpy = sandbox.spy(tray, 'setImage');
const getImagePath = (): string => {
const result = setContextMenuSpy.lastCall?.firstArg;
const result = setImageSpy.lastCall?.firstArg;
if (!result) {
throw new Error('Expected tray.setImage to be called at least once');
}
Expand All @@ -230,4 +230,27 @@ describe('SystemTrayService', () => {
service.setUnreadCount(0);
assert.match(path.parse(getImagePath()).base, /^icon_\d+\.png$/);
});

it('uses a fallback image if the icon file cannot be found', () => {
const service = newService();
service.setEnabled(true);
service.setMainWindow(new BrowserWindow({ show: false }));

const tray = service._getTray();
if (!tray) {
throw new Error('Test setup failed: expected a tray');
}

const setImageStub = sandbox.stub(tray, 'setImage');
setImageStub.withArgs(sinon.match.string).throws('Failed to load');

service.setUnreadCount(4);

// Electron doesn't export this class, so we have to wrestle it out.
const NativeImage = nativeImage.createEmpty().constructor;

sinon.assert.calledTwice(setImageStub);
sinon.assert.calledWith(setImageStub, sinon.match.string);
sinon.assert.calledWith(setImageStub, sinon.match.instanceOf(NativeImage));
});
});

0 comments on commit 54fe694

Please sign in to comment.