Minimize and start Signal in tray

This commit is contained in:
Fedor Indutny 2022-09-06 15:09:52 -07:00 committed by GitHub
parent aa86d8bf82
commit b54c6f257d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 152 additions and 127 deletions

View file

@ -3067,6 +3067,14 @@
"message": "Unanswered video call",
"description": "Shown in conversation history when your video call is missed or declined"
},
"minimizeToTrayNotification--title": {
"message": "Signal is still running",
"description": "Shown in a notification title when Signal is minimized to tray"
},
"minimizeToTrayNotification--body": {
"message": "Signal will keep running in the notification area. You can change this in Signal settings.",
"description": "Shown in a notification body when Signal is minimized to tray"
},
"incomingAudioCall": {
"message": "Incoming audio call...",
"description": "Shown in both the incoming call bar and notification for an incoming audio call"

View file

@ -68,7 +68,7 @@ export class SystemTraySettingCache {
result = parseSystemTraySetting(value);
log.info(`getSystemTraySetting returning ${result}`);
} else {
result = SystemTraySetting.DoNotUseSystemTray;
result = SystemTraySetting.Uninitialized;
log.info(`getSystemTraySetting got no value, returning ${result}`);
}

View file

@ -29,10 +29,12 @@ import {
session,
shell,
systemPreferences,
Notification,
} from 'electron';
import type {
MenuItemConstructorOptions,
TitleBarOverlayOptions,
LoginItemSettingsOptions,
} from 'electron';
import { z } from 'zod';
@ -82,6 +84,7 @@ import {
shouldMinimizeToSystemTray,
parseSystemTraySetting,
} from '../ts/types/SystemTraySetting';
import { isSystemTraySupported } from '../ts/types/Settings';
import * as ephemeralConfig from './ephemeral_config';
import * as logging from '../ts/logging/main_process_logging';
import { MainSQL } from '../ts/sql/main';
@ -856,6 +859,23 @@ async function createWindow() {
await systemTraySettingCache.get()
);
if (!windowState.shouldQuit() && (usingTrayIcon || OS.isMacOS())) {
if (usingTrayIcon) {
const shownTrayNotice = ephemeralConfig.get('shown-tray-notice');
if (shownTrayNotice) {
getLogger().info('close: not showing tray notice');
return;
}
ephemeralConfig.set('shown-tray-notice', true);
getLogger().info('close: showing tray notice');
const n = new Notification({
title: getLocale().i18n('minimizeToTrayNotification--title'),
body: getLocale().i18n('minimizeToTrayNotification--body'),
});
n.show();
}
return;
}
@ -867,6 +887,22 @@ async function createWindow() {
app.quit();
});
mainWindow.on('minimize', async () => {
if (!mainWindow) {
getLogger().info('minimize event: no main window');
return;
}
// When tray icon is in use - close the window since it will be minimized
// to tray anyway.
const usingTrayIcon = shouldMinimizeToSystemTray(
await systemTraySettingCache.get()
);
if (usingTrayIcon) {
mainWindow.close();
}
});
// Emitted when the window is closed.
mainWindow.on('closed', () => {
// Dereference the window object, usually you would store windows
@ -1566,6 +1602,26 @@ function getAppLocale(): string {
return getEnvironment() === Environment.Test ? 'en' : app.getLocale();
}
async function getDefaultLoginItemSettings(): Promise<LoginItemSettingsOptions> {
if (!OS.isWindows()) {
return {};
}
const systemTraySetting = await systemTraySettingCache.get();
if (
systemTraySetting !== SystemTraySetting.MinimizeToSystemTray &&
// This is true when we just started with `--start-in-tray`
systemTraySetting !== SystemTraySetting.MinimizeToAndStartInSystemTray
) {
return {};
}
// The effect of this is that if both auto-launch and minimize to system tray
// are enabled on Windows - we will start the app in tray automatically,
// letting the Desktop shortcuts still start the Signal not in tray.
return { args: ['--start-in-tray'] };
}
// Signal doesn't really use media keys so we set this switch here to unblock
// them so that other apps can use them if they need to.
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling');
@ -1596,6 +1652,36 @@ app.on('ready', async () => {
sqlInitPromise = initializeSQL(userDataPath);
// First run: configure Signal to minimize to tray. Additionally, on Windows
// enable auto-start with start-in-tray so that starting from a Desktop icon
// would still show the window.
// (User can change these settings later)
if (
isSystemTraySupported(app.getVersion()) &&
(await systemTraySettingCache.get()) === SystemTraySetting.Uninitialized
) {
const newValue = SystemTraySetting.MinimizeToSystemTray;
getLogger().info(`app.ready: setting system-tray-setting to ${newValue}`);
systemTraySettingCache.set(newValue);
// Update both stores
ephemeralConfig.set('system-tray-setting', newValue);
await sql.sqlCall('createOrUpdateItem', [
{
id: 'system-tray-setting',
value: newValue,
},
]);
if (OS.isWindows()) {
getLogger().info('app.ready: enabling open at login');
app.setLoginItemSettings({
...(await getDefaultLoginItemSettings()),
openAtLogin: true,
});
}
}
const startTime = Date.now();
settingsChannel = new SettingsChannel();
@ -2055,9 +2141,13 @@ ipc.on(
}
);
ipc.on(
ipc.handle(
'update-system-tray-setting',
(_event, rawSystemTraySetting /* : Readonly<unknown> */) => {
async (_event, rawSystemTraySetting /* : Readonly<unknown> */) => {
const { openAtLogin } = app.getLoginItemSettings(
await getDefaultLoginItemSettings()
);
const systemTraySetting = parseSystemTraySetting(rawSystemTraySetting);
systemTraySettingCache.set(systemTraySetting);
@ -2065,6 +2155,13 @@ ipc.on(
const isEnabled = shouldMinimizeToSystemTray(systemTraySetting);
systemTrayService.setEnabled(isEnabled);
}
// Default login item settings might have changed, so update the object.
getLogger().info('refresh-auto-launch: new value', openAtLogin);
app.setLoginItemSettings({
...(await getDefaultLoginItemSettings()),
openAtLogin,
});
}
);
@ -2290,11 +2387,17 @@ async function ensureFilePermissions(onlyFiles?: Array<string>) {
}
ipc.handle('get-auto-launch', async () => {
return app.getLoginItemSettings().openAtLogin;
return app.getLoginItemSettings(await getDefaultLoginItemSettings())
.openAtLogin;
});
ipc.handle('set-auto-launch', async (_event, value) => {
app.setLoginItemSettings({ openAtLogin: Boolean(value) });
const openAtLogin = Boolean(value);
getLogger().info('set-auto-launch: new value', openAtLogin);
app.setLoginItemSettings({
...(await getDefaultLoginItemSettings()),
openAtLogin,
});
});
ipc.on('show-message-box', (_event, { type, message }) => {

View file

@ -98,6 +98,7 @@ const getDefaultArgs = (): PropsDataType => ({
isPhoneNumberSharingSupported: false,
isSyncSupported: true,
isSystemTraySupported: true,
isMinimizeToAndStartInSystemTraySupported: true,
lastSyncTime: Date.now(),
notificationContent: 'name',
selectedCamera:

View file

@ -95,6 +95,7 @@ export type PropsDataType = {
isPhoneNumberSharingSupported: boolean;
isSyncSupported: boolean;
isSystemTraySupported: boolean;
isMinimizeToAndStartInSystemTraySupported: boolean;
availableCameras: Array<
Pick<MediaDeviceInfo, 'deviceId' | 'groupId' | 'kind' | 'label'>
@ -239,6 +240,7 @@ export const Preferences = ({
isNotificationAttentionSupported,
isSyncSupported,
isSystemTraySupported,
isMinimizeToAndStartInSystemTraySupported,
hasCustomTitleBar,
lastSyncTime,
makeSyncRequest,
@ -371,16 +373,18 @@ export const Preferences = ({
name="system-tray-setting-minimize-to-system-tray"
onChange={onMinimizeToSystemTrayChange}
/>
<Checkbox
checked={hasMinimizeToAndStartInSystemTray}
disabled={!hasMinimizeToSystemTray}
label={i18n(
'SystemTraySetting__minimize-to-and-start-in-system-tray'
)}
moduleClassName="Preferences__checkbox"
name="system-tray-setting-minimize-to-and-start-in-system-tray"
onChange={onMinimizeToAndStartInSystemTrayChange}
/>
{isMinimizeToAndStartInSystemTraySupported && (
<Checkbox
checked={hasMinimizeToAndStartInSystemTray}
disabled={!hasMinimizeToSystemTray}
label={i18n(
'SystemTraySetting__minimize-to-and-start-in-system-tray'
)}
moduleClassName="Preferences__checkbox"
name="system-tray-setting-minimize-to-and-start-in-system-tray"
onChange={onMinimizeToAndStartInSystemTrayChange}
/>
)}
</>
)}
</SettingsRow>

View file

@ -1,100 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ChangeEvent, FunctionComponent } from 'react';
import React, { useState } from 'react';
import {
SystemTraySetting,
parseSystemTraySetting,
shouldMinimizeToSystemTray,
} from '../../types/SystemTraySetting';
import type { 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>
</>
);
};

View file

@ -22,7 +22,6 @@ import { MessageDetail } from './components/conversation/MessageDetail';
import { Quote } from './components/conversation/Quote';
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
import { SystemTraySettingsCheckboxes } from './components/conversation/SystemTraySettingsCheckboxes';
// State
import { createChatColorPicker } from './state/roots/createChatColorPicker';
@ -409,7 +408,6 @@ export const setup = (options: {
Quote,
StagedLinkPreview,
DisappearingTimeDialog,
SystemTraySettingsCheckboxes,
};
const Roots = {

View file

@ -78,32 +78,32 @@ describe('SystemTraySettingCache', () => {
sinon.assert.notCalled(configSetStub);
});
it('returns DoNotUseSystemTray if system tray is supported but no preference is stored', async () => {
it('returns Uninitialized if system tray is supported but no preference is stored', async () => {
sandbox.stub(process, 'platform').value('win32');
const cache = new SystemTraySettingCache(sql, config, [], '1.2.3');
assert.strictEqual(await cache.get(), SystemTraySetting.DoNotUseSystemTray);
assert.strictEqual(await cache.get(), SystemTraySetting.Uninitialized);
assert(configGetStub.calledOnceWith('system-tray-setting'));
assert(
configSetStub.calledOnceWith(
'system-tray-setting',
SystemTraySetting.DoNotUseSystemTray
SystemTraySetting.Uninitialized
)
);
});
it('returns DoNotUseSystemTray if system tray is supported but the stored preference is invalid', async () => {
it('returns Uninitialized 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, config, [], '1.2.3');
assert.strictEqual(await cache.get(), SystemTraySetting.DoNotUseSystemTray);
assert.strictEqual(await cache.get(), SystemTraySetting.Uninitialized);
assert(configGetStub.calledOnceWith('system-tray-setting'));
assert(
configSetStub.calledOnceWith(
'system-tray-setting',
SystemTraySetting.DoNotUseSystemTray
SystemTraySetting.Uninitialized
)
);
});

View file

@ -45,10 +45,10 @@ describe('system tray setting utilities', () => {
);
});
it('parses invalid strings to DoNotUseSystemTray', () => {
it('parses invalid strings to Uninitialized', () => {
assert.strictEqual(
parseSystemTraySetting('garbage'),
SystemTraySetting.DoNotUseSystemTray
SystemTraySetting.Uninitialized
);
});
});

View file

@ -53,6 +53,12 @@ export const isSystemTraySupported = (appVersion: string): boolean =>
// We eventually want to support Linux in production.
OS.isWindows() || (OS.isLinux() && !isProduction(appVersion));
// On Windows minimize and start in system tray is default when app is selected
// to launch at login, because we can provide `['--start-in-tray']` args.
export const isMinimizeToAndStartInSystemTraySupported = (
appVersion: string
): boolean => !OS.isWindows() && isSystemTraySupported(appVersion);
export const isAutoDownloadUpdatesSupported = (): boolean =>
OS.isWindows() || OS.isMacOS();

View file

@ -5,6 +5,7 @@ import { makeEnumParser } from '../util/enum';
// Be careful when changing these values, as they are persisted.
export enum SystemTraySetting {
Uninitialized = 'Uninitialized',
DoNotUseSystemTray = 'DoNotUseSystemTray',
MinimizeToSystemTray = 'MinimizeToSystemTray',
MinimizeToAndStartInSystemTray = 'MinimizeToAndStartInSystemTray',
@ -18,5 +19,5 @@ export const shouldMinimizeToSystemTray = (
export const parseSystemTraySetting = makeEnumParser(
SystemTraySetting,
SystemTraySetting.DoNotUseSystemTray
SystemTraySetting.Uninitialized
);

View file

@ -154,7 +154,7 @@ window.setMenuBarVisibility = visibility =>
window.updateSystemTraySetting = (
systemTraySetting /* : Readonly<SystemTraySetting> */
) => {
ipc.send('update-system-tray-setting', systemTraySetting);
ipc.invoke('update-system-tray-setting', systemTraySetting);
};
window.restart = () => {

View file

@ -285,6 +285,10 @@ const renderPreferences = async () => {
isSystemTraySupported: Settings.isSystemTraySupported(
SignalContext.getVersion()
),
isMinimizeToAndStartInSystemTraySupported:
Settings.isMinimizeToAndStartInSystemTraySupported(
SignalContext.getVersion()
),
// Change handlers
onAudioNotificationsChange: reRender(settingAudioNotification.setValue),