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
|
@ -55,6 +55,10 @@ import { ReadReceipts } from './messageModifiers/ReadReceipts';
|
|||
import { ReadSyncs } from './messageModifiers/ReadSyncs';
|
||||
import { ViewSyncs } from './messageModifiers/ViewSyncs';
|
||||
import * as AttachmentDownloads from './messageModifiers/AttachmentDownloads';
|
||||
import {
|
||||
SystemTraySetting,
|
||||
parseSystemTraySetting,
|
||||
} from './types/SystemTraySetting';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
||||
|
@ -464,6 +468,12 @@ export async function startApp(): Promise<void> {
|
|||
window.setAutoHideMenuBar(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: () =>
|
||||
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 { createStream } from 'rotating-file-stream';
|
||||
|
||||
import { setLogAtLevel } from './log';
|
||||
import { Environment, getEnvironment } from '../environment';
|
||||
|
||||
import {
|
||||
|
@ -327,6 +328,8 @@ function isProbablyObjectHasBeenDestroyedError(err: unknown): boolean {
|
|||
|
||||
// This blows up using mocha --watch, so we ensure it is run just once
|
||||
if (!console._log) {
|
||||
setLogAtLevel(logAtLevel);
|
||||
|
||||
console._log = console.log;
|
||||
console.log = _.partial(logAtLevel, LogLevel.Info);
|
||||
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
|
||||
|
||||
import os from 'os';
|
||||
|
@ -167,4 +167,22 @@ describe('Settings', () => {
|
|||
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`.
|
||||
export const getTitleBarVisibility = (): TitleBarVisibility =>
|
||||
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 { RetryItemType } from '../util/retryPlaceholders';
|
||||
import type { ConfigMapType as RemoteConfigType } from '../RemoteConfig';
|
||||
import { SystemTraySetting } from './SystemTraySetting';
|
||||
|
||||
import type { GroupCredentialType } from '../textsecure/WebAPI';
|
||||
import type {
|
||||
|
@ -32,6 +33,7 @@ export type StorageAccessType = {
|
|||
'call-ringtone-notification': boolean;
|
||||
'call-system-notification': boolean;
|
||||
'hide-menu-bar': boolean;
|
||||
'system-tray-setting': SystemTraySetting;
|
||||
'incoming-call-notification': boolean;
|
||||
'notification-draw-attention': boolean;
|
||||
'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",
|
||||
"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(",
|
||||
"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 { isValidGuid } from './util/isValidGuid';
|
||||
import { StateType } from './state/reducer';
|
||||
import { SystemTraySetting } from './types/SystemTraySetting';
|
||||
|
||||
export { Long } from 'long';
|
||||
|
||||
|
@ -251,6 +252,7 @@ declare global {
|
|||
setAutoHideMenuBar: (value: WhatIsThis) => void;
|
||||
setBadgeCount: (count: number) => void;
|
||||
setMenuBarVisibility: (value: WhatIsThis) => void;
|
||||
updateSystemTraySetting: (value: SystemTraySetting) => void;
|
||||
showConfirmationDialog: (options: ConfirmationDialogViewProps) => void;
|
||||
showKeyboardShortcuts: () => void;
|
||||
storage: Storage;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue