Officially support the system tray on Windows
This commit is contained in:
parent
23acbf284e
commit
af1f2ea449
24 changed files with 968 additions and 194 deletions
|
@ -1527,6 +1527,14 @@
|
||||||
"message": "Spell check will be disabled the next time Signal starts.",
|
"message": "Spell check will be disabled the next time Signal starts.",
|
||||||
"description": "Shown when the user disables spellcheck to indicate that they must restart Signal."
|
"description": "Shown when the user disables spellcheck to indicate that they must restart Signal."
|
||||||
},
|
},
|
||||||
|
"SystemTraySetting__minimize-to-system-tray": {
|
||||||
|
"message": "Minimize to system tray",
|
||||||
|
"description": "In the settings, shown next to the checkbox option for minimizing to the system tray"
|
||||||
|
},
|
||||||
|
"SystemTraySetting__minimize-to-and-start-in-system-tray": {
|
||||||
|
"message": "Start minimized to tray",
|
||||||
|
"description": "In the settings, shown next to the checkbox option for starting in the system tray"
|
||||||
|
},
|
||||||
"autoLaunchDescription": {
|
"autoLaunchDescription": {
|
||||||
"message": "Open at computer login",
|
"message": "Open at computer login",
|
||||||
"description": "Description for the automatic launch setting"
|
"description": "Description for the automatic launch setting"
|
||||||
|
|
233
app/SystemTrayService.ts
Normal file
233
app/SystemTrayService.ts
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
// Copyright 2017-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
import { BrowserWindow, app, Menu, Tray } from 'electron';
|
||||||
|
import * as log from '../ts/logging/log';
|
||||||
|
import type { LocaleMessagesType } from '../ts/types/I18N';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that manages an [Electron `Tray` instance][0]. It's responsible for creating
|
||||||
|
* and destroying a `Tray`, and listening to the associated `BrowserWindow`'s visibility
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* [0]: https://www.electronjs.org/docs/api/tray
|
||||||
|
*/
|
||||||
|
export class SystemTrayService {
|
||||||
|
private browserWindow?: BrowserWindow;
|
||||||
|
|
||||||
|
private readonly messages: LocaleMessagesType;
|
||||||
|
|
||||||
|
private tray?: Tray;
|
||||||
|
|
||||||
|
private isEnabled = false;
|
||||||
|
|
||||||
|
private unreadCount = 0;
|
||||||
|
|
||||||
|
private boundRender: typeof SystemTrayService.prototype.render;
|
||||||
|
|
||||||
|
constructor({ messages }: Readonly<{ messages: LocaleMessagesType }>) {
|
||||||
|
log.info('System tray service: created');
|
||||||
|
this.messages = messages;
|
||||||
|
this.boundRender = this.render.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update or clear the associated `BrowserWindow`. This is used for the hide/show
|
||||||
|
* functionality. It attaches event listeners to the window to manage the hide/show
|
||||||
|
* toggle in the tray's context menu.
|
||||||
|
*/
|
||||||
|
setMainWindow(newBrowserWindow: undefined | BrowserWindow): void {
|
||||||
|
const oldBrowserWindow = this.browserWindow;
|
||||||
|
if (oldBrowserWindow === newBrowserWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`System tray service: updating main window. Previously, there was ${
|
||||||
|
oldBrowserWindow ? '' : 'not '
|
||||||
|
}a window, and now there is${newBrowserWindow ? '' : ' not'}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (oldBrowserWindow) {
|
||||||
|
oldBrowserWindow.off('show', this.boundRender);
|
||||||
|
oldBrowserWindow.off('hide', this.boundRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newBrowserWindow) {
|
||||||
|
newBrowserWindow.on('show', this.boundRender);
|
||||||
|
newBrowserWindow.on('hide', this.boundRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.browserWindow = newBrowserWindow;
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable the tray icon. Note: if there is no associated browser window (see
|
||||||
|
* `setMainWindow`), the tray icon will not be shown, even if enabled.
|
||||||
|
*/
|
||||||
|
setEnabled(isEnabled: boolean): void {
|
||||||
|
if (this.isEnabled === isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`System tray service: ${isEnabled ? 'enabling' : 'disabling'}`);
|
||||||
|
this.isEnabled = isEnabled;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the unread count, which updates the tray icon if it's visible.
|
||||||
|
*/
|
||||||
|
setUnreadCount(unreadCount: number): void {
|
||||||
|
if (this.unreadCount === unreadCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`System tray service: setting unread count to ${unreadCount}`);
|
||||||
|
this.unreadCount = unreadCount;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private render(): void {
|
||||||
|
if (this.isEnabled && this.browserWindow) {
|
||||||
|
this.renderEnabled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.renderDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderEnabled() {
|
||||||
|
log.info('System tray service: rendering the tray');
|
||||||
|
|
||||||
|
this.tray = this.tray || this.createTray();
|
||||||
|
const { browserWindow, tray } = this;
|
||||||
|
|
||||||
|
tray.setImage(getIcon(this.unreadCount));
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// For details please refer to:
|
||||||
|
// https://github.com/electron/electron/blob/master/docs/api/tray.md.
|
||||||
|
tray.setContextMenu(
|
||||||
|
Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
id: 'toggleWindowVisibility',
|
||||||
|
...(browserWindow?.isVisible()
|
||||||
|
? {
|
||||||
|
label: this.messages.hide.message,
|
||||||
|
click: () => {
|
||||||
|
log.info(
|
||||||
|
'System tray service: hiding the window from the context menu'
|
||||||
|
);
|
||||||
|
// We re-fetch `this.browserWindow` here just in case the browser window
|
||||||
|
// has changed while the context menu was open. Same applies in the
|
||||||
|
// "show" case below.
|
||||||
|
this.browserWindow?.hide();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: this.messages.show.message,
|
||||||
|
click: () => {
|
||||||
|
log.info(
|
||||||
|
'System tray service: showing the window from the context menu'
|
||||||
|
);
|
||||||
|
if (this.browserWindow) {
|
||||||
|
this.browserWindow.show();
|
||||||
|
forceOnTop(this.browserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quit',
|
||||||
|
label: this.messages.quit.message,
|
||||||
|
click: () => {
|
||||||
|
log.info(
|
||||||
|
'System tray service: quitting the app from the context menu'
|
||||||
|
);
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDisabled() {
|
||||||
|
log.info('System tray service: rendering no tray');
|
||||||
|
|
||||||
|
if (!this.tray) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.tray.destroy();
|
||||||
|
this.tray = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createTray(): Tray {
|
||||||
|
log.info('System tray service: creating the tray');
|
||||||
|
|
||||||
|
// This icon may be swiftly overwritten.
|
||||||
|
const result = new Tray(getIcon(this.unreadCount));
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// the app indicator is set up.
|
||||||
|
//
|
||||||
|
// See <https://github.com/electron/electron/blob/v13.1.3/docs/api/tray.md#class-tray>.
|
||||||
|
result.on('click', () => {
|
||||||
|
const { browserWindow } = this;
|
||||||
|
if (!browserWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!browserWindow.isVisible()) {
|
||||||
|
browserWindow.show();
|
||||||
|
}
|
||||||
|
forceOnTop(browserWindow);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.setToolTip(this.messages.signalDesktop.message);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is exported for testing, because Electron doesn't have any easy way to hook
|
||||||
|
* into the existing tray instances. It should not be used by "real" code.
|
||||||
|
*/
|
||||||
|
_getTray(): undefined | Tray {
|
||||||
|
return this.tray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(unreadCount: number) {
|
||||||
|
let iconSize: string;
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
iconSize = '16';
|
||||||
|
break;
|
||||||
|
case 'win32':
|
||||||
|
iconSize = '32';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
iconSize = '256';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unreadCount > 0) {
|
||||||
|
const filename = `${String(unreadCount >= 10 ? 10 : unreadCount)}.png`;
|
||||||
|
return join(__dirname, '..', 'images', 'alert', iconSize, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return join(__dirname, '..', 'images', `icon_${iconSize}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceOnTop(browserWindow: BrowserWindow) {
|
||||||
|
// On some versions of GNOME the window may not be on top when restored.
|
||||||
|
// This trick should fix it.
|
||||||
|
// Thanks to: https://github.com/Enrico204/Whatsapp-Desktop/commit/6b0dc86b64e481b455f8fce9b4d797e86d000dc1
|
||||||
|
browserWindow.setAlwaysOnTop(true);
|
||||||
|
browserWindow.focus();
|
||||||
|
browserWindow.setAlwaysOnTop(false);
|
||||||
|
}
|
84
app/SystemTraySettingCache.ts
Normal file
84
app/SystemTraySettingCache.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright 2017-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as log from '../ts/logging/log';
|
||||||
|
import {
|
||||||
|
parseSystemTraySetting,
|
||||||
|
SystemTraySetting,
|
||||||
|
} from '../ts/types/SystemTraySetting';
|
||||||
|
import { isSystemTraySupported } from '../ts/types/Settings';
|
||||||
|
import type { MainSQL } from '../ts/sql/main';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small helper class to get and cache the `system-tray-setting` preference in the main
|
||||||
|
* process.
|
||||||
|
*/
|
||||||
|
export class SystemTraySettingCache {
|
||||||
|
private cachedValue: undefined | SystemTraySetting;
|
||||||
|
|
||||||
|
private getPromise: undefined | Promise<SystemTraySetting>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly sql: Pick<MainSQL, 'sqlCall'>,
|
||||||
|
private readonly argv: Array<string>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async get(): Promise<SystemTraySetting> {
|
||||||
|
if (this.cachedValue !== undefined) {
|
||||||
|
return this.cachedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getPromise = this.getPromise || this.doFirstGet();
|
||||||
|
return this.getPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(value: SystemTraySetting): void {
|
||||||
|
this.cachedValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doFirstGet(): Promise<SystemTraySetting> {
|
||||||
|
let result: SystemTraySetting;
|
||||||
|
|
||||||
|
// These command line flags are not officially supported, but many users rely on them.
|
||||||
|
// Be careful when removing them or making changes.
|
||||||
|
if (this.argv.some(arg => arg === '--start-in-tray')) {
|
||||||
|
result = SystemTraySetting.MinimizeToAndStartInSystemTray;
|
||||||
|
log.info(
|
||||||
|
`getSystemTraySetting saw --start-in-tray flag. Returning ${result}`
|
||||||
|
);
|
||||||
|
} else if (this.argv.some(arg => arg === '--use-tray-icon')) {
|
||||||
|
result = SystemTraySetting.MinimizeToSystemTray;
|
||||||
|
log.info(
|
||||||
|
`getSystemTraySetting saw --use-tray-icon flag. Returning ${result}`
|
||||||
|
);
|
||||||
|
} else if (isSystemTraySupported()) {
|
||||||
|
const { value } = (await this.sql.sqlCall('getItemById', [
|
||||||
|
'system-tray-setting',
|
||||||
|
])) || { value: undefined };
|
||||||
|
|
||||||
|
if (value !== undefined) {
|
||||||
|
result = parseSystemTraySetting(value);
|
||||||
|
log.info(
|
||||||
|
`getSystemTraySetting returning value from database, ${result}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = SystemTraySetting.DoNotUseSystemTray;
|
||||||
|
log.info(
|
||||||
|
`getSystemTraySetting got no value from database, returning ${result}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = SystemTraySetting.DoNotUseSystemTray;
|
||||||
|
log.info(
|
||||||
|
`getSystemTraySetting had no flags and did no DB lookups. Returning ${result}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a value in the cache, someone has updated the value "out from under us",
|
||||||
|
// so we should return that because it's newer.
|
||||||
|
this.cachedValue =
|
||||||
|
this.cachedValue === undefined ? result : this.cachedValue;
|
||||||
|
|
||||||
|
return this.cachedValue;
|
||||||
|
}
|
||||||
|
}
|
138
app/tray_icon.ts
138
app/tray_icon.ts
|
@ -1,138 +0,0 @@
|
||||||
// Copyright 2017-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { join } from 'path';
|
|
||||||
import { existsSync } from 'fs';
|
|
||||||
|
|
||||||
import { BrowserWindow, app, Menu, Tray } from 'electron';
|
|
||||||
import * as DockIcon from '../ts/dock_icon';
|
|
||||||
|
|
||||||
import { LocaleMessagesType } from '../ts/types/I18N';
|
|
||||||
|
|
||||||
let trayContextMenu = null;
|
|
||||||
let tray: Tray | undefined;
|
|
||||||
|
|
||||||
export default function createTrayIcon(
|
|
||||||
getMainWindow: () => BrowserWindow | undefined,
|
|
||||||
messages: LocaleMessagesType
|
|
||||||
): { updateContextMenu: () => void; updateIcon: (count: number) => void } {
|
|
||||||
let iconSize: string;
|
|
||||||
switch (process.platform) {
|
|
||||||
case 'darwin':
|
|
||||||
iconSize = '16';
|
|
||||||
break;
|
|
||||||
case 'win32':
|
|
||||||
iconSize = '32';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
iconSize = '256';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconNoNewMessages = join(
|
|
||||||
__dirname,
|
|
||||||
'..',
|
|
||||||
'images',
|
|
||||||
`icon_${iconSize}.png`
|
|
||||||
);
|
|
||||||
|
|
||||||
tray = new Tray(iconNoNewMessages);
|
|
||||||
|
|
||||||
const forceOnTop = (mainWindow: BrowserWindow) => {
|
|
||||||
if (mainWindow) {
|
|
||||||
// On some versions of GNOME the window may not be on top when restored.
|
|
||||||
// This trick should fix it.
|
|
||||||
// Thanks to: https://github.com/Enrico204/Whatsapp-Desktop/commit/6b0dc86b64e481b455f8fce9b4d797e86d000dc1
|
|
||||||
mainWindow.setAlwaysOnTop(true);
|
|
||||||
mainWindow.focus();
|
|
||||||
mainWindow.setAlwaysOnTop(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleWindowVisibility = () => {
|
|
||||||
const mainWindow = getMainWindow();
|
|
||||||
if (mainWindow) {
|
|
||||||
if (mainWindow.isVisible()) {
|
|
||||||
mainWindow.hide();
|
|
||||||
DockIcon.hide();
|
|
||||||
} else {
|
|
||||||
mainWindow.show();
|
|
||||||
DockIcon.show();
|
|
||||||
|
|
||||||
forceOnTop(mainWindow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateContextMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
const showWindow = () => {
|
|
||||||
const mainWindow = getMainWindow();
|
|
||||||
if (mainWindow) {
|
|
||||||
if (!mainWindow.isVisible()) {
|
|
||||||
mainWindow.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
forceOnTop(mainWindow);
|
|
||||||
}
|
|
||||||
updateContextMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateContextMenu = () => {
|
|
||||||
const mainWindow = getMainWindow();
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// For details please refer to:
|
|
||||||
// https://github.com/electron/electron/blob/master/docs/api/tray.md.
|
|
||||||
trayContextMenu = Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
id: 'toggleWindowVisibility',
|
|
||||||
label:
|
|
||||||
messages[mainWindow && mainWindow.isVisible() ? 'hide' : 'show']
|
|
||||||
.message,
|
|
||||||
click: toggleWindowVisibility,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'quit',
|
|
||||||
label: messages.quit.message,
|
|
||||||
click: app.quit.bind(app),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
tray?.setContextMenu(trayContextMenu);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateIcon = (unreadCount: number) => {
|
|
||||||
let image;
|
|
||||||
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
const filename = `${String(unreadCount >= 10 ? 10 : unreadCount)}.png`;
|
|
||||||
image = join(__dirname, '..', 'images', 'alert', iconSize, filename);
|
|
||||||
} else {
|
|
||||||
image = iconNoNewMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(image)) {
|
|
||||||
console.log('tray.updateIcon: Image for tray update does not exist!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
tray?.setImage(image);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
'tray.setImage error:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tray.on('click', showWindow);
|
|
||||||
|
|
||||||
tray.setToolTip(messages.signalDesktop.message);
|
|
||||||
updateContextMenu();
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateContextMenu,
|
|
||||||
updateIcon,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -57,6 +57,9 @@ const {
|
||||||
const {
|
const {
|
||||||
DisappearingTimeDialog,
|
DisappearingTimeDialog,
|
||||||
} = require('../../ts/components/DisappearingTimeDialog');
|
} = require('../../ts/components/DisappearingTimeDialog');
|
||||||
|
const {
|
||||||
|
SystemTraySettingsCheckboxes,
|
||||||
|
} = require('../../ts/components/conversation/SystemTraySettingsCheckboxes');
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
||||||
|
@ -341,6 +344,7 @@ exports.setup = (options = {}) => {
|
||||||
ProgressModal,
|
ProgressModal,
|
||||||
StagedLinkPreview,
|
StagedLinkPreview,
|
||||||
DisappearingTimeDialog,
|
DisappearingTimeDialog,
|
||||||
|
SystemTraySettingsCheckboxes,
|
||||||
Types: {
|
Types: {
|
||||||
Message: MediaGalleryMessage,
|
Message: MediaGalleryMessage,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* global $, Whisper */
|
/* global $, Whisper */
|
||||||
|
@ -29,6 +29,7 @@ const getInitialData = async () => ({
|
||||||
|
|
||||||
themeSetting: await window.getThemeSetting(),
|
themeSetting: await window.getThemeSetting(),
|
||||||
hideMenuBar: await window.getHideMenuBar(),
|
hideMenuBar: await window.getHideMenuBar(),
|
||||||
|
systemTray: await window.getSystemTraySetting(),
|
||||||
|
|
||||||
notificationSetting: await window.getNotificationSetting(),
|
notificationSetting: await window.getNotificationSetting(),
|
||||||
audioNotification: await window.getAudioNotification(),
|
audioNotification: await window.getAudioNotification(),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2016-2020 Signal Messenger, LLC
|
// Copyright 2016-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* global i18n: false */
|
/* global i18n: false */
|
||||||
|
@ -274,6 +274,16 @@
|
||||||
setFn: window.setHideMenuBar,
|
setFn: window.setHideMenuBar,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
new window.Whisper.ReactWrapperView({
|
||||||
|
el: this.$('.system-tray-setting-container'),
|
||||||
|
Component: window.Signal.Components.SystemTraySettingsCheckboxes,
|
||||||
|
props: {
|
||||||
|
i18n,
|
||||||
|
initialValue: window.initialData.systemTray,
|
||||||
|
isSystemTraySupported: Settings.isSystemTraySupported(),
|
||||||
|
onChange: window.setSystemTraySetting,
|
||||||
|
},
|
||||||
|
});
|
||||||
new CheckboxView({
|
new CheckboxView({
|
||||||
el: this.$('.always-relay-calls-setting'),
|
el: this.$('.always-relay-calls-setting'),
|
||||||
name: 'always-relay-calls-setting',
|
name: 'always-relay-calls-setting',
|
||||||
|
|
88
main.js
88
main.js
|
@ -65,12 +65,6 @@ function getMainWindow() {
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tray icon and related objects
|
|
||||||
let tray = null;
|
|
||||||
const startInTray = process.argv.some(arg => arg === '--start-in-tray');
|
|
||||||
const usingTrayIcon =
|
|
||||||
startInTray || process.argv.some(arg => arg === '--use-tray-icon');
|
|
||||||
|
|
||||||
const config = require('./app/config').default;
|
const config = require('./app/config').default;
|
||||||
|
|
||||||
// Very important to put before the single instance check, since it is based on the
|
// Very important to put before the single instance check, since it is based on the
|
||||||
|
@ -91,8 +85,13 @@ const attachments = require('./app/attachments');
|
||||||
const attachmentChannel = require('./app/attachment_channel');
|
const attachmentChannel = require('./app/attachment_channel');
|
||||||
const bounce = require('./ts/services/bounce');
|
const bounce = require('./ts/services/bounce');
|
||||||
const updater = require('./ts/updater/index');
|
const updater = require('./ts/updater/index');
|
||||||
const createTrayIcon = require('./app/tray_icon').default;
|
const { SystemTrayService } = require('./app/SystemTrayService');
|
||||||
const dockIcon = require('./ts/dock_icon');
|
const { SystemTraySettingCache } = require('./app/SystemTraySettingCache');
|
||||||
|
const {
|
||||||
|
SystemTraySetting,
|
||||||
|
shouldMinimizeToSystemTray,
|
||||||
|
parseSystemTraySetting,
|
||||||
|
} = require('./ts/types/SystemTraySetting');
|
||||||
const ephemeralConfig = require('./app/ephemeral_config');
|
const ephemeralConfig = require('./app/ephemeral_config');
|
||||||
const logging = require('./ts/logging/main_process_logging');
|
const logging = require('./ts/logging/main_process_logging');
|
||||||
const { MainSQL } = require('./ts/sql/main');
|
const { MainSQL } = require('./ts/sql/main');
|
||||||
|
@ -127,6 +126,10 @@ const { PowerChannel } = require('./ts/main/powerChannel');
|
||||||
const { maybeParseUrl, setUrlSearchParams } = require('./ts/util/url');
|
const { maybeParseUrl, setUrlSearchParams } = require('./ts/util/url');
|
||||||
|
|
||||||
const sql = new MainSQL();
|
const sql = new MainSQL();
|
||||||
|
|
||||||
|
let systemTrayService;
|
||||||
|
const systemTraySettingCache = new SystemTraySettingCache(sql, process.argv);
|
||||||
|
|
||||||
const challengeHandler = new ChallengeMainHandler();
|
const challengeHandler = new ChallengeMainHandler();
|
||||||
|
|
||||||
let sqlInitTimeStart = 0;
|
let sqlInitTimeStart = 0;
|
||||||
|
@ -174,14 +177,6 @@ function showWindow() {
|
||||||
} else {
|
} else {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggle the visibility of the show/hide tray icon menu entries
|
|
||||||
if (tray) {
|
|
||||||
tray.updateContextMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
// show the app on the Dock in case it was hidden before
|
|
||||||
dockIcon.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.mas) {
|
if (!process.mas) {
|
||||||
|
@ -394,6 +389,10 @@ async function createWindow() {
|
||||||
delete windowOptions.autoHideMenuBar;
|
delete windowOptions.autoHideMenuBar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startInTray =
|
||||||
|
(await systemTraySettingCache.get()) ===
|
||||||
|
SystemTraySetting.MinimizeToAndStartInSystemTray;
|
||||||
|
|
||||||
const visibleOnAnyScreen = _.some(screen.getAllDisplays(), display => {
|
const visibleOnAnyScreen = _.some(screen.getAllDisplays(), display => {
|
||||||
if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) {
|
if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -416,12 +415,15 @@ async function createWindow() {
|
||||||
mainWindow = new BrowserWindow(windowOptions);
|
mainWindow = new BrowserWindow(windowOptions);
|
||||||
mainWindowCreated = true;
|
mainWindowCreated = true;
|
||||||
setupSpellChecker(mainWindow, locale.messages);
|
setupSpellChecker(mainWindow, locale.messages);
|
||||||
if (!usingTrayIcon && windowConfig && windowConfig.maximized) {
|
if (!startInTray && windowConfig) {
|
||||||
mainWindow.maximize();
|
mainWindow.maximize();
|
||||||
}
|
}
|
||||||
if (!usingTrayIcon && windowConfig && windowConfig.fullscreen) {
|
if (!startInTray && windowConfig && windowConfig.fullscreen) {
|
||||||
mainWindow.setFullScreen(true);
|
mainWindow.setFullScreen(true);
|
||||||
}
|
}
|
||||||
|
if (systemTrayService) {
|
||||||
|
systemTrayService.setMainWindow(mainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
function captureAndSaveWindowStats() {
|
function captureAndSaveWindowStats() {
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
|
@ -533,17 +535,10 @@ async function createWindow() {
|
||||||
|
|
||||||
// On Mac, or on other platforms when the tray icon is in use, the window
|
// On Mac, or on other platforms when the tray icon is in use, the window
|
||||||
// should be only hidden, not closed, when the user clicks the close button
|
// should be only hidden, not closed, when the user clicks the close button
|
||||||
|
const usingTrayIcon = shouldMinimizeToSystemTray(
|
||||||
|
await systemTraySettingCache.get()
|
||||||
|
);
|
||||||
if (!windowState.shouldQuit() && (usingTrayIcon || OS.isMacOS())) {
|
if (!windowState.shouldQuit() && (usingTrayIcon || OS.isMacOS())) {
|
||||||
// toggle the visibility of the show/hide tray icon menu entries
|
|
||||||
if (tray) {
|
|
||||||
tray.updateContextMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide the app from the Dock on macOS if the tray icon is enabled
|
|
||||||
if (usingTrayIcon) {
|
|
||||||
dockIcon.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,7 +555,10 @@ async function createWindow() {
|
||||||
// Dereference the window object, usually you would store windows
|
// Dereference the window object, usually you would store windows
|
||||||
// in an array if your app supports multi windows, this is the time
|
// in an array if your app supports multi windows, this is the time
|
||||||
// when you should delete the corresponding element.
|
// when you should delete the corresponding element.
|
||||||
mainWindow = null;
|
mainWindow = undefined;
|
||||||
|
if (systemTrayService) {
|
||||||
|
systemTrayService.setMainWindow(mainWindow);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on('enter-full-screen', () => {
|
mainWindow.on('enter-full-screen', () => {
|
||||||
|
@ -580,7 +578,6 @@ async function createWindow() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// allow to start minimised in tray
|
|
||||||
if (!startInTray) {
|
if (!startInTray) {
|
||||||
console.log('showing main window');
|
console.log('showing main window');
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
|
@ -1360,12 +1357,14 @@ app.on('ready', async () => {
|
||||||
|
|
||||||
ready = true;
|
ready = true;
|
||||||
|
|
||||||
if (usingTrayIcon) {
|
|
||||||
tray = createTrayIcon(getMainWindow, locale.messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMenu();
|
setupMenu();
|
||||||
|
|
||||||
|
systemTrayService = new SystemTrayService({ messages: locale.messages });
|
||||||
|
systemTrayService.setMainWindow(mainWindow);
|
||||||
|
systemTrayService.setEnabled(
|
||||||
|
shouldMinimizeToSystemTray(await systemTraySettingCache.get())
|
||||||
|
);
|
||||||
|
|
||||||
ensureFilePermissions([
|
ensureFilePermissions([
|
||||||
'config.json',
|
'config.json',
|
||||||
'sql/db.sqlite',
|
'sql/db.sqlite',
|
||||||
|
@ -1561,6 +1560,19 @@ ipc.on('set-menu-bar-visibility', (event, visibility) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipc.on('update-system-tray-setting', (
|
||||||
|
_event,
|
||||||
|
rawSystemTraySetting /* : Readonly<unknown> */
|
||||||
|
) => {
|
||||||
|
const systemTraySetting = parseSystemTraySetting(rawSystemTraySetting);
|
||||||
|
systemTraySettingCache.set(systemTraySetting);
|
||||||
|
|
||||||
|
if (systemTrayService) {
|
||||||
|
const isEnabled = shouldMinimizeToSystemTray(systemTraySetting);
|
||||||
|
systemTrayService.setEnabled(isEnabled);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipc.on('close-about', () => {
|
ipc.on('close-about', () => {
|
||||||
if (aboutWindow) {
|
if (aboutWindow) {
|
||||||
aboutWindow.close();
|
aboutWindow.close();
|
||||||
|
@ -1583,9 +1595,9 @@ ipc.on('show-screen-share', (event, sourceName) => {
|
||||||
showScreenShareWindow(sourceName);
|
showScreenShareWindow(sourceName);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('update-tray-icon', (event, unreadCount) => {
|
ipc.on('update-tray-icon', (_event, unreadCount) => {
|
||||||
if (tray) {
|
if (systemTrayService) {
|
||||||
tray.updateIcon(unreadCount);
|
systemTrayService.setUnreadCount(unreadCount);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1645,6 +1657,8 @@ installSettingsGetter('theme-setting');
|
||||||
installSettingsSetter('theme-setting');
|
installSettingsSetter('theme-setting');
|
||||||
installSettingsGetter('hide-menu-bar');
|
installSettingsGetter('hide-menu-bar');
|
||||||
installSettingsSetter('hide-menu-bar');
|
installSettingsSetter('hide-menu-bar');
|
||||||
|
installSettingsGetter('system-tray-setting');
|
||||||
|
installSettingsSetter('system-tray-setting');
|
||||||
|
|
||||||
installSettingsGetter('notification-setting');
|
installSettingsGetter('notification-setting');
|
||||||
installSettingsSetter('notification-setting');
|
installSettingsSetter('notification-setting');
|
||||||
|
|
|
@ -157,6 +157,12 @@ try {
|
||||||
window.setMenuBarVisibility = visibility =>
|
window.setMenuBarVisibility = visibility =>
|
||||||
ipc.send('set-menu-bar-visibility', visibility);
|
ipc.send('set-menu-bar-visibility', visibility);
|
||||||
|
|
||||||
|
window.updateSystemTraySetting = (
|
||||||
|
systemTraySetting /* : Readonly<SystemTraySetting> */
|
||||||
|
) => {
|
||||||
|
ipc.send('update-system-tray-setting', systemTraySetting);
|
||||||
|
};
|
||||||
|
|
||||||
window.restart = () => {
|
window.restart = () => {
|
||||||
window.log.info('restart');
|
window.log.info('restart');
|
||||||
ipc.send('restart');
|
ipc.send('restart');
|
||||||
|
@ -231,6 +237,8 @@ try {
|
||||||
installSetter('theme-setting', 'setThemeSetting');
|
installSetter('theme-setting', 'setThemeSetting');
|
||||||
installGetter('hide-menu-bar', 'getHideMenuBar');
|
installGetter('hide-menu-bar', 'getHideMenuBar');
|
||||||
installSetter('hide-menu-bar', 'setHideMenuBar');
|
installSetter('hide-menu-bar', 'setHideMenuBar');
|
||||||
|
installGetter('system-tray-setting', 'getSystemTraySetting');
|
||||||
|
installSetter('system-tray-setting', 'setSystemTraySetting');
|
||||||
|
|
||||||
installGetter('notification-setting', 'getNotificationSetting');
|
installGetter('notification-setting', 'getNotificationSetting');
|
||||||
installSetter('notification-setting', 'setNotificationSetting');
|
installSetter('notification-setting', 'setNotificationSetting');
|
||||||
|
|
|
@ -157,6 +157,7 @@
|
||||||
{{ spellCheckDirtyText }}
|
{{ spellCheckDirtyText }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="system-tray-setting-container"></div>
|
||||||
{{ #isAutoLaunchSupported }}
|
{{ #isAutoLaunchSupported }}
|
||||||
<div class='auto-launch-setting'>
|
<div class='auto-launch-setting'>
|
||||||
<input type='checkbox' name='auto-launch-setting' id='auto-launch-setting' />
|
<input type='checkbox' name='auto-launch-setting' id='auto-launch-setting' />
|
||||||
|
|
|
@ -64,6 +64,8 @@ window.getThemeSetting = makeGetter('theme-setting');
|
||||||
window.setThemeSetting = makeSetter('theme-setting');
|
window.setThemeSetting = makeSetter('theme-setting');
|
||||||
window.getHideMenuBar = makeGetter('hide-menu-bar');
|
window.getHideMenuBar = makeGetter('hide-menu-bar');
|
||||||
window.setHideMenuBar = makeSetter('hide-menu-bar');
|
window.setHideMenuBar = makeSetter('hide-menu-bar');
|
||||||
|
window.getSystemTraySetting = makeGetter('system-tray-setting');
|
||||||
|
window.setSystemTraySetting = makeSetter('system-tray-setting');
|
||||||
|
|
||||||
window.getSpellCheck = makeGetter('spell-check');
|
window.getSpellCheck = makeGetter('spell-check');
|
||||||
window.setSpellCheck = makeSetter('spell-check');
|
window.setSpellCheck = makeSetter('spell-check');
|
||||||
|
|
|
@ -55,6 +55,10 @@ import { ReadReceipts } from './messageModifiers/ReadReceipts';
|
||||||
import { ReadSyncs } from './messageModifiers/ReadSyncs';
|
import { ReadSyncs } from './messageModifiers/ReadSyncs';
|
||||||
import { ViewSyncs } from './messageModifiers/ViewSyncs';
|
import { ViewSyncs } from './messageModifiers/ViewSyncs';
|
||||||
import * as AttachmentDownloads from './messageModifiers/AttachmentDownloads';
|
import * as AttachmentDownloads from './messageModifiers/AttachmentDownloads';
|
||||||
|
import {
|
||||||
|
SystemTraySetting,
|
||||||
|
parseSystemTraySetting,
|
||||||
|
} from './types/SystemTraySetting';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||||
|
|
||||||
|
@ -464,6 +468,12 @@ export async function startApp(): Promise<void> {
|
||||||
window.setAutoHideMenuBar(value);
|
window.setAutoHideMenuBar(value);
|
||||||
window.setMenuBarVisibility(!value);
|
window.setMenuBarVisibility(!value);
|
||||||
},
|
},
|
||||||
|
getSystemTraySetting: (): SystemTraySetting =>
|
||||||
|
parseSystemTraySetting(window.storage.get('system-tray-setting')),
|
||||||
|
setSystemTraySetting: (value: Readonly<SystemTraySetting>) => {
|
||||||
|
window.storage.put('system-tray-setting', value);
|
||||||
|
window.updateSystemTraySetting(value);
|
||||||
|
},
|
||||||
|
|
||||||
getNotificationSetting: () =>
|
getNotificationSetting: () =>
|
||||||
window.storage.get('notification-setting', 'message'),
|
window.storage.get('notification-setting', 'message'),
|
||||||
|
|
99
ts/components/conversation/SystemTraySettingsCheckboxes.tsx
Normal file
99
ts/components/conversation/SystemTraySettingsCheckboxes.tsx
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { ChangeEvent, FunctionComponent, useState } from 'react';
|
||||||
|
import {
|
||||||
|
SystemTraySetting,
|
||||||
|
parseSystemTraySetting,
|
||||||
|
shouldMinimizeToSystemTray,
|
||||||
|
} from '../../types/SystemTraySetting';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
initialValue: string;
|
||||||
|
isSystemTraySupported: boolean;
|
||||||
|
onChange: (value: SystemTraySetting) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This component is rendered by Backbone, so it deviates from idiomatic React a bit. For
|
||||||
|
// example, it does not receive its value as a prop.
|
||||||
|
export const SystemTraySettingsCheckboxes: FunctionComponent<PropsType> = ({
|
||||||
|
i18n,
|
||||||
|
initialValue,
|
||||||
|
isSystemTraySupported,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const [localValue, setLocalValue] = useState<SystemTraySetting>(
|
||||||
|
parseSystemTraySetting(initialValue)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isSystemTraySupported) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setValue = (value: SystemTraySetting): void => {
|
||||||
|
setLocalValue(oldValue => {
|
||||||
|
if (oldValue !== value) {
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMinimizeToSystemTray = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setValue(
|
||||||
|
event.target.checked
|
||||||
|
? SystemTraySetting.MinimizeToSystemTray
|
||||||
|
: SystemTraySetting.DoNotUseSystemTray
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMinimizeToAndStartInSystemTray = (
|
||||||
|
event: ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
setValue(
|
||||||
|
event.target.checked
|
||||||
|
? SystemTraySetting.MinimizeToAndStartInSystemTray
|
||||||
|
: SystemTraySetting.MinimizeToSystemTray
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const minimizesToTray = shouldMinimizeToSystemTray(localValue);
|
||||||
|
const minimizesToAndStartsInSystemTray =
|
||||||
|
localValue === SystemTraySetting.MinimizeToAndStartInSystemTray;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
checked={minimizesToTray}
|
||||||
|
id="system-tray-setting-minimize-to-system-tray"
|
||||||
|
onChange={setMinimizeToSystemTray}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
{/* These manual spaces mirror the non-React parts of the settings screen. */}{' '}
|
||||||
|
<label htmlFor="system-tray-setting-minimize-to-system-tray">
|
||||||
|
{i18n('SystemTraySetting__minimize-to-system-tray')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
checked={minimizesToAndStartsInSystemTray}
|
||||||
|
disabled={!minimizesToTray}
|
||||||
|
id="system-tray-setting-minimize-to-and-start-in-system-tray"
|
||||||
|
onChange={setMinimizeToAndStartInSystemTray}
|
||||||
|
type="checkbox"
|
||||||
|
/>{' '}
|
||||||
|
{/* These styles should live in CSS, but because we intend to rewrite the settings
|
||||||
|
screen, this inline CSS limits the scope of the future rewrite. */}
|
||||||
|
<label
|
||||||
|
htmlFor="system-tray-setting-minimize-to-and-start-in-system-tray"
|
||||||
|
style={minimizesToTray ? {} : { opacity: 0.75 }}
|
||||||
|
>
|
||||||
|
{i18n('SystemTraySetting__minimize-to-and-start-in-system-tray')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,16 +0,0 @@
|
||||||
// Copyright 2018-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { app } from 'electron';
|
|
||||||
|
|
||||||
export function show(): void {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
app.dock.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hide(): void {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
app.dock.hide();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,6 +17,7 @@ import { read as readLastLines } from 'read-last-lines';
|
||||||
import rimraf from 'rimraf';
|
import rimraf from 'rimraf';
|
||||||
import { createStream } from 'rotating-file-stream';
|
import { createStream } from 'rotating-file-stream';
|
||||||
|
|
||||||
|
import { setLogAtLevel } from './log';
|
||||||
import { Environment, getEnvironment } from '../environment';
|
import { Environment, getEnvironment } from '../environment';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -327,6 +328,8 @@ function isProbablyObjectHasBeenDestroyedError(err: unknown): boolean {
|
||||||
|
|
||||||
// This blows up using mocha --watch, so we ensure it is run just once
|
// This blows up using mocha --watch, so we ensure it is run just once
|
||||||
if (!console._log) {
|
if (!console._log) {
|
||||||
|
setLogAtLevel(logAtLevel);
|
||||||
|
|
||||||
console._log = console.log;
|
console._log = console.log;
|
||||||
console.log = _.partial(logAtLevel, LogLevel.Info);
|
console.log = _.partial(logAtLevel, LogLevel.Info);
|
||||||
console._error = console.error;
|
console._error = console.error;
|
||||||
|
|
233
ts/test-node/app/SystemTrayService_test.ts
Normal file
233
ts/test-node/app/SystemTrayService_test.ts
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import { BrowserWindow, MenuItem, Tray } from 'electron';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { SystemTrayService } from '../../../app/SystemTrayService';
|
||||||
|
|
||||||
|
describe('SystemTrayService', () => {
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiating an Electron `Tray` has side-effects that we need to clean up. Make sure
|
||||||
|
* to use `newService` instead of `new SystemTrayService` in these tests to ensure that
|
||||||
|
* the tray is cleaned up.
|
||||||
|
*
|
||||||
|
* This only affects these tests, not the "real" code.
|
||||||
|
*/
|
||||||
|
function newService(): SystemTrayService {
|
||||||
|
const result = new SystemTrayService({
|
||||||
|
messages: {
|
||||||
|
hide: { message: 'Hide' },
|
||||||
|
quit: { message: 'Quit' },
|
||||||
|
show: { message: 'Show' },
|
||||||
|
signalDesktop: { message: 'Signal' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
servicesCreated.add(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const servicesCreated = new Set<SystemTrayService>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
|
||||||
|
servicesCreated.forEach(service => {
|
||||||
|
service._getTray()?.destroy();
|
||||||
|
});
|
||||||
|
servicesCreated.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't render a tray icon unless (1) we're enabled (2) there's a browser window", () => {
|
||||||
|
const service = newService();
|
||||||
|
assert.isUndefined(service._getTray());
|
||||||
|
|
||||||
|
service.setEnabled(true);
|
||||||
|
assert.isUndefined(service._getTray());
|
||||||
|
|
||||||
|
service.setMainWindow(new BrowserWindow({ show: false }));
|
||||||
|
assert.instanceOf(service._getTray(), Tray);
|
||||||
|
|
||||||
|
service.setEnabled(false);
|
||||||
|
assert.isUndefined(service._getTray());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a "Hide" button when the window is shown and a "Show" button when the window is hidden', () => {
|
||||||
|
// We don't actually want to show a browser window. It's disruptive when you're
|
||||||
|
// running tests and can introduce test-only flakiness. We jump through some hoops
|
||||||
|
// to fake the behavior.
|
||||||
|
let fakeIsVisible = false;
|
||||||
|
const browserWindow = new BrowserWindow({ show: fakeIsVisible });
|
||||||
|
sinon.stub(browserWindow, 'isVisible').callsFake(() => fakeIsVisible);
|
||||||
|
sinon.stub(browserWindow, 'show').callsFake(() => {
|
||||||
|
fakeIsVisible = true;
|
||||||
|
browserWindow.emit('show');
|
||||||
|
});
|
||||||
|
sinon.stub(browserWindow, 'hide').callsFake(() => {
|
||||||
|
fakeIsVisible = false;
|
||||||
|
browserWindow.emit('hide');
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = newService();
|
||||||
|
service.setEnabled(true);
|
||||||
|
service.setMainWindow(browserWindow);
|
||||||
|
|
||||||
|
const tray = service._getTray();
|
||||||
|
if (!tray) {
|
||||||
|
throw new Error('Test setup failed: expected a tray');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ideally, there'd be something like `tray.getContextMenu`, but that doesn't exist.
|
||||||
|
// We also can't spy on `Tray.prototype.setContextMenu` because it's not defined
|
||||||
|
// that way. So we spy on the specific instance, just to get the context menu.
|
||||||
|
const setContextMenuSpy = sandbox.spy(tray, 'setContextMenu');
|
||||||
|
const getToggleMenuItem = (): undefined | null | MenuItem =>
|
||||||
|
setContextMenuSpy.lastCall?.firstArg?.getMenuItemById(
|
||||||
|
'toggleWindowVisibility'
|
||||||
|
);
|
||||||
|
|
||||||
|
browserWindow.show();
|
||||||
|
assert.strictEqual(getToggleMenuItem()?.label, 'Hide');
|
||||||
|
|
||||||
|
getToggleMenuItem()?.click();
|
||||||
|
assert.strictEqual(getToggleMenuItem()?.label, 'Show');
|
||||||
|
|
||||||
|
getToggleMenuItem()?.click();
|
||||||
|
assert.strictEqual(getToggleMenuItem()?.label, 'Hide');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('destroys the tray when disabling', () => {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.isFalse(tray.isDestroyed());
|
||||||
|
|
||||||
|
service.setEnabled(false);
|
||||||
|
|
||||||
|
assert.isTrue(tray.isDestroyed());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maintains the same Tray instance when switching browser window instances', () => {
|
||||||
|
const service = newService();
|
||||||
|
service.setEnabled(true);
|
||||||
|
service.setMainWindow(new BrowserWindow({ show: false }));
|
||||||
|
|
||||||
|
const originalTray = service._getTray();
|
||||||
|
|
||||||
|
service.setMainWindow(new BrowserWindow({ show: false }));
|
||||||
|
|
||||||
|
assert.strictEqual(service._getTray(), originalTray);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes browser window event listeners when changing browser window instances', () => {
|
||||||
|
const firstBrowserWindow = new BrowserWindow({ show: false });
|
||||||
|
|
||||||
|
const showListenersAtStart = firstBrowserWindow.listenerCount('show');
|
||||||
|
const hideListenersAtStart = firstBrowserWindow.listenerCount('hide');
|
||||||
|
|
||||||
|
const service = newService();
|
||||||
|
service.setEnabled(true);
|
||||||
|
service.setMainWindow(firstBrowserWindow);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
firstBrowserWindow.listenerCount('show'),
|
||||||
|
showListenersAtStart + 1
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
firstBrowserWindow.listenerCount('hide'),
|
||||||
|
hideListenersAtStart + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
service.setMainWindow(new BrowserWindow({ show: false }));
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
firstBrowserWindow.listenerCount('show'),
|
||||||
|
showListenersAtStart
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
firstBrowserWindow.listenerCount('hide'),
|
||||||
|
hideListenersAtStart
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes browser window event listeners when removing browser window instances', () => {
|
||||||
|
const browserWindow = new BrowserWindow({ show: false });
|
||||||
|
|
||||||
|
const showListenersAtStart = browserWindow.listenerCount('show');
|
||||||
|
const hideListenersAtStart = browserWindow.listenerCount('hide');
|
||||||
|
|
||||||
|
const service = newService();
|
||||||
|
service.setEnabled(true);
|
||||||
|
service.setMainWindow(browserWindow);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
browserWindow.listenerCount('show'),
|
||||||
|
showListenersAtStart + 1
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
browserWindow.listenerCount('hide'),
|
||||||
|
hideListenersAtStart + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
service.setMainWindow(undefined);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
browserWindow.listenerCount('show'),
|
||||||
|
showListenersAtStart
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
browserWindow.listenerCount('hide'),
|
||||||
|
hideListenersAtStart
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the icon when the unread count changes', () => {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 getImagePath = (): string => {
|
||||||
|
const result = setContextMenuSpy.lastCall?.firstArg;
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Expected tray.setImage to be called at least once');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 9; i >= 1; i -= 1) {
|
||||||
|
service.setUnreadCount(i);
|
||||||
|
assert.strictEqual(path.parse(getImagePath()).base, `${i}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 10; i < 13; i += 1) {
|
||||||
|
service.setUnreadCount(i);
|
||||||
|
assert.strictEqual(path.parse(getImagePath()).base, '10.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
service.setUnreadCount(0);
|
||||||
|
assert.match(path.parse(getImagePath()).base, /^icon_\d+\.png$/);
|
||||||
|
});
|
||||||
|
});
|
103
ts/test-node/app/SystemTraySettingCache_test.ts
Normal file
103
ts/test-node/app/SystemTraySettingCache_test.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import { MainSQL } from '../../sql/main';
|
||||||
|
import { SystemTraySetting } from '../../types/SystemTraySetting';
|
||||||
|
|
||||||
|
import { SystemTraySettingCache } from '../../../app/SystemTraySettingCache';
|
||||||
|
|
||||||
|
describe('SystemTraySettingCache', () => {
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
|
||||||
|
let sqlCallStub: sinon.SinonStub;
|
||||||
|
let sql: Pick<MainSQL, 'sqlCall'>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox();
|
||||||
|
|
||||||
|
sqlCallStub = sandbox.stub().resolves();
|
||||||
|
sql = { sqlCall: sqlCallStub };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns MinimizeToAndStartInSystemTray if passed the --start-in-tray argument', async () => {
|
||||||
|
const justOneArg = new SystemTraySettingCache(sql, ['--start-in-tray']);
|
||||||
|
assert.strictEqual(
|
||||||
|
await justOneArg.get(),
|
||||||
|
SystemTraySetting.MinimizeToAndStartInSystemTray
|
||||||
|
);
|
||||||
|
|
||||||
|
const bothArgs = new SystemTraySettingCache(sql, [
|
||||||
|
'--start-in-tray',
|
||||||
|
'--use-tray-icon',
|
||||||
|
]);
|
||||||
|
assert.strictEqual(
|
||||||
|
await bothArgs.get(),
|
||||||
|
SystemTraySetting.MinimizeToAndStartInSystemTray
|
||||||
|
);
|
||||||
|
|
||||||
|
sinon.assert.notCalled(sqlCallStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns MinimizeToSystemTray if passed the --use-tray-icon argument', async () => {
|
||||||
|
const cache = new SystemTraySettingCache(sql, ['--use-tray-icon']);
|
||||||
|
assert.strictEqual(
|
||||||
|
await cache.get(),
|
||||||
|
SystemTraySetting.MinimizeToSystemTray
|
||||||
|
);
|
||||||
|
|
||||||
|
sinon.assert.notCalled(sqlCallStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns DoNotUseSystemTray if system tray is supported but no preference is stored', async () => {
|
||||||
|
sandbox.stub(process, 'platform').value('win32');
|
||||||
|
|
||||||
|
const cache = new SystemTraySettingCache(sql, []);
|
||||||
|
assert.strictEqual(await cache.get(), SystemTraySetting.DoNotUseSystemTray);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns DoNotUseSystemTray if system tray is supported but the stored preference is invalid', async () => {
|
||||||
|
sandbox.stub(process, 'platform').value('win32');
|
||||||
|
|
||||||
|
sqlCallStub.resolves({ value: 'garbage' });
|
||||||
|
|
||||||
|
const cache = new SystemTraySettingCache(sql, []);
|
||||||
|
assert.strictEqual(await cache.get(), SystemTraySetting.DoNotUseSystemTray);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the stored preference if system tray is supported and something is stored', async () => {
|
||||||
|
sandbox.stub(process, 'platform').value('win32');
|
||||||
|
|
||||||
|
sqlCallStub.resolves({ value: 'MinimizeToSystemTray' });
|
||||||
|
|
||||||
|
const cache = new SystemTraySettingCache(sql, []);
|
||||||
|
assert.strictEqual(
|
||||||
|
await cache.get(),
|
||||||
|
SystemTraySetting.MinimizeToSystemTray
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only kicks off one request to the database if multiple sources ask at once', async () => {
|
||||||
|
sandbox.stub(process, 'platform').value('win32');
|
||||||
|
|
||||||
|
const cache = new SystemTraySettingCache(sql, []);
|
||||||
|
|
||||||
|
await Promise.all([cache.get(), cache.get(), cache.get()]);
|
||||||
|
|
||||||
|
sinon.assert.calledOnce(sqlCallStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns DoNotUseSystemTray if system tray is unsupported and there are no CLI flags', async () => {
|
||||||
|
sandbox.stub(process, 'platform').value('darwin');
|
||||||
|
|
||||||
|
const cache = new SystemTraySettingCache(sql, []);
|
||||||
|
assert.strictEqual(await cache.get(), SystemTraySetting.DoNotUseSystemTray);
|
||||||
|
|
||||||
|
sinon.assert.notCalled(sqlCallStub);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
@ -167,4 +167,22 @@ describe('Settings', () => {
|
||||||
assert.isTrue(Settings.isDrawAttentionSupported());
|
assert.isTrue(Settings.isDrawAttentionSupported());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isSystemTraySupported', () => {
|
||||||
|
it('returns false on macOS', () => {
|
||||||
|
sandbox.stub(process, 'platform').value('darwin');
|
||||||
|
assert.isFalse(Settings.isSystemTraySupported());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true on Windows 8', () => {
|
||||||
|
sandbox.stub(process, 'platform').value('win32');
|
||||||
|
sandbox.stub(os, 'release').returns('8.0.0');
|
||||||
|
assert.isTrue(Settings.isSystemTraySupported());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on Linux', () => {
|
||||||
|
sandbox.stub(process, 'platform').value('linux');
|
||||||
|
assert.isFalse(Settings.isSystemTraySupported());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
55
ts/test-node/types/SystemTraySetting_test.ts
Normal file
55
ts/test-node/types/SystemTraySetting_test.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SystemTraySetting,
|
||||||
|
parseSystemTraySetting,
|
||||||
|
shouldMinimizeToSystemTray,
|
||||||
|
} from '../../types/SystemTraySetting';
|
||||||
|
|
||||||
|
describe('system tray setting utilities', () => {
|
||||||
|
describe('shouldMinimizeToSystemTray', () => {
|
||||||
|
it('returns false if the system tray is disabled', () => {
|
||||||
|
assert.isFalse(
|
||||||
|
shouldMinimizeToSystemTray(SystemTraySetting.DoNotUseSystemTray)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if the system tray is enabled', () => {
|
||||||
|
assert.isTrue(
|
||||||
|
shouldMinimizeToSystemTray(SystemTraySetting.MinimizeToSystemTray)
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
shouldMinimizeToSystemTray(
|
||||||
|
SystemTraySetting.MinimizeToAndStartInSystemTray
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseSystemTraySetting', () => {
|
||||||
|
it('parses valid strings into their enum values', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
parseSystemTraySetting('DoNotUseSystemTray'),
|
||||||
|
SystemTraySetting.DoNotUseSystemTray
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
parseSystemTraySetting('MinimizeToSystemTray'),
|
||||||
|
SystemTraySetting.MinimizeToSystemTray
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
parseSystemTraySetting('MinimizeToAndStartInSystemTray'),
|
||||||
|
SystemTraySetting.MinimizeToAndStartInSystemTray
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses invalid strings to DoNotUseSystemTray', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
parseSystemTraySetting('garbage'),
|
||||||
|
SystemTraySetting.DoNotUseSystemTray
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -49,3 +49,11 @@ export enum TitleBarVisibility {
|
||||||
// This should match the "logic" in `stylesheets/_global.scss`.
|
// This should match the "logic" in `stylesheets/_global.scss`.
|
||||||
export const getTitleBarVisibility = (): TitleBarVisibility =>
|
export const getTitleBarVisibility = (): TitleBarVisibility =>
|
||||||
OS.isMacOS() ? TitleBarVisibility.Hidden : TitleBarVisibility.Visible;
|
OS.isMacOS() ? TitleBarVisibility.Hidden : TitleBarVisibility.Visible;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if you can minimize the app to the system tray. Users can override this
|
||||||
|
* option with a command line flag, but that is not officially supported.
|
||||||
|
*
|
||||||
|
* We may add support for Linux in the future.
|
||||||
|
*/
|
||||||
|
export const isSystemTraySupported = OS.isWindows;
|
||||||
|
|
2
ts/types/Storage.d.ts
vendored
2
ts/types/Storage.d.ts
vendored
|
@ -10,6 +10,7 @@ import type { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverabil
|
||||||
import type { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
import type { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
||||||
import type { RetryItemType } from '../util/retryPlaceholders';
|
import type { RetryItemType } from '../util/retryPlaceholders';
|
||||||
import type { ConfigMapType as RemoteConfigType } from '../RemoteConfig';
|
import type { ConfigMapType as RemoteConfigType } from '../RemoteConfig';
|
||||||
|
import { SystemTraySetting } from './SystemTraySetting';
|
||||||
|
|
||||||
import type { GroupCredentialType } from '../textsecure/WebAPI';
|
import type { GroupCredentialType } from '../textsecure/WebAPI';
|
||||||
import type {
|
import type {
|
||||||
|
@ -32,6 +33,7 @@ export type StorageAccessType = {
|
||||||
'call-ringtone-notification': boolean;
|
'call-ringtone-notification': boolean;
|
||||||
'call-system-notification': boolean;
|
'call-system-notification': boolean;
|
||||||
'hide-menu-bar': boolean;
|
'hide-menu-bar': boolean;
|
||||||
|
'system-tray-setting': SystemTraySetting;
|
||||||
'incoming-call-notification': boolean;
|
'incoming-call-notification': boolean;
|
||||||
'notification-draw-attention': boolean;
|
'notification-draw-attention': boolean;
|
||||||
'notification-setting': 'message' | 'name' | 'count' | 'off';
|
'notification-setting': 'message' | 'name' | 'count' | 'off';
|
||||||
|
|
22
ts/types/SystemTraySetting.ts
Normal file
22
ts/types/SystemTraySetting.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { makeEnumParser } from '../util/enum';
|
||||||
|
|
||||||
|
// Be careful when changing these values, as they are persisted.
|
||||||
|
export enum SystemTraySetting {
|
||||||
|
DoNotUseSystemTray = 'DoNotUseSystemTray',
|
||||||
|
MinimizeToSystemTray = 'MinimizeToSystemTray',
|
||||||
|
MinimizeToAndStartInSystemTray = 'MinimizeToAndStartInSystemTray',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shouldMinimizeToSystemTray = (
|
||||||
|
setting: SystemTraySetting
|
||||||
|
): boolean =>
|
||||||
|
setting === SystemTraySetting.MinimizeToSystemTray ||
|
||||||
|
setting === SystemTraySetting.MinimizeToAndStartInSystemTray;
|
||||||
|
|
||||||
|
export const parseSystemTraySetting = makeEnumParser(
|
||||||
|
SystemTraySetting,
|
||||||
|
SystemTraySetting.DoNotUseSystemTray
|
||||||
|
);
|
|
@ -1059,6 +1059,14 @@
|
||||||
"updated": "2021-05-27T01:33:06.541Z",
|
"updated": "2021-05-27T01:33:06.541Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "js/views/settings_view.js",
|
||||||
|
"line": " el: this.$('.system-tray-setting-container'),",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-06-24T23:16:24.537Z",
|
||||||
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/settings_view.js",
|
"path": "js/views/settings_view.js",
|
||||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -121,6 +121,7 @@ import { ConversationColorType, CustomColorType } from './types/Colors';
|
||||||
import { MessageController } from './util/MessageController';
|
import { MessageController } from './util/MessageController';
|
||||||
import { isValidGuid } from './util/isValidGuid';
|
import { isValidGuid } from './util/isValidGuid';
|
||||||
import { StateType } from './state/reducer';
|
import { StateType } from './state/reducer';
|
||||||
|
import { SystemTraySetting } from './types/SystemTraySetting';
|
||||||
|
|
||||||
export { Long } from 'long';
|
export { Long } from 'long';
|
||||||
|
|
||||||
|
@ -251,6 +252,7 @@ declare global {
|
||||||
setAutoHideMenuBar: (value: WhatIsThis) => void;
|
setAutoHideMenuBar: (value: WhatIsThis) => void;
|
||||||
setBadgeCount: (count: number) => void;
|
setBadgeCount: (count: number) => void;
|
||||||
setMenuBarVisibility: (value: WhatIsThis) => void;
|
setMenuBarVisibility: (value: WhatIsThis) => void;
|
||||||
|
updateSystemTraySetting: (value: SystemTraySetting) => void;
|
||||||
showConfirmationDialog: (options: ConfirmationDialogViewProps) => void;
|
showConfirmationDialog: (options: ConfirmationDialogViewProps) => void;
|
||||||
showKeyboardShortcuts: () => void;
|
showKeyboardShortcuts: () => void;
|
||||||
storage: Storage;
|
storage: Storage;
|
||||||
|
|
Loading…
Reference in a new issue