Minimize and start Signal in tray
This commit is contained in:
parent
aa86d8bf82
commit
b54c6f257d
13 changed files with 152 additions and 127 deletions
|
@ -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"
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
||||
|
|
111
app/main.ts
111
app/main.ts
|
@ -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 }) => {
|
||||
|
|
|
@ -98,6 +98,7 @@ const getDefaultArgs = (): PropsDataType => ({
|
|||
isPhoneNumberSharingSupported: false,
|
||||
isSyncSupported: true,
|
||||
isSystemTraySupported: true,
|
||||
isMinimizeToAndStartInSystemTraySupported: true,
|
||||
lastSyncTime: Date.now(),
|
||||
notificationContent: 'name',
|
||||
selectedCamera:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -285,6 +285,10 @@ const renderPreferences = async () => {
|
|||
isSystemTraySupported: Settings.isSystemTraySupported(
|
||||
SignalContext.getVersion()
|
||||
),
|
||||
isMinimizeToAndStartInSystemTraySupported:
|
||||
Settings.isMinimizeToAndStartInSystemTraySupported(
|
||||
SignalContext.getVersion()
|
||||
),
|
||||
|
||||
// Change handlers
|
||||
onAudioNotificationsChange: reRender(settingAudioNotification.setValue),
|
||||
|
|
Loading…
Reference in a new issue