From b54c6f257dbf31ab25a7a61390b5a1df85567594 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Tue, 6 Sep 2022 15:09:52 -0700 Subject: [PATCH] Minimize and start Signal in tray --- _locales/en/messages.json | 8 ++ app/SystemTraySettingCache.ts | 2 +- app/main.ts | 111 +++++++++++++++++- ts/components/Preferences.stories.tsx | 1 + ts/components/Preferences.tsx | 24 ++-- .../SystemTraySettingsCheckboxes.tsx | 100 ---------------- ts/signal.ts | 2 - .../app/SystemTraySettingCache_test.ts | 12 +- ts/test-node/types/SystemTraySetting_test.ts | 4 +- ts/types/Settings.ts | 6 + ts/types/SystemTraySetting.ts | 3 +- ts/windows/main/phase1-ipc.ts | 2 +- ts/windows/settings/preload.ts | 4 + 13 files changed, 152 insertions(+), 127 deletions(-) delete mode 100644 ts/components/conversation/SystemTraySettingsCheckboxes.tsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7a60500a5..8d52d0391 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/app/SystemTraySettingCache.ts b/app/SystemTraySettingCache.ts index 6b49d22a6..19a3b843e 100644 --- a/app/SystemTraySettingCache.ts +++ b/app/SystemTraySettingCache.ts @@ -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}`); } diff --git a/app/main.ts b/app/main.ts index a95d2af98..7f16d3cac 100644 --- a/app/main.ts +++ b/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 { + 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 */) => { + async (_event, rawSystemTraySetting /* : Readonly */) => { + 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) { } 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 }) => { diff --git a/ts/components/Preferences.stories.tsx b/ts/components/Preferences.stories.tsx index c1a375821..5cdeba7fb 100644 --- a/ts/components/Preferences.stories.tsx +++ b/ts/components/Preferences.stories.tsx @@ -98,6 +98,7 @@ const getDefaultArgs = (): PropsDataType => ({ isPhoneNumberSharingSupported: false, isSyncSupported: true, isSystemTraySupported: true, + isMinimizeToAndStartInSystemTraySupported: true, lastSyncTime: Date.now(), notificationContent: 'name', selectedCamera: diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index bc2a2a581..54cbc61f7 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -95,6 +95,7 @@ export type PropsDataType = { isPhoneNumberSharingSupported: boolean; isSyncSupported: boolean; isSystemTraySupported: boolean; + isMinimizeToAndStartInSystemTraySupported: boolean; availableCameras: Array< Pick @@ -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} /> - + {isMinimizeToAndStartInSystemTraySupported && ( + + )} )} diff --git a/ts/components/conversation/SystemTraySettingsCheckboxes.tsx b/ts/components/conversation/SystemTraySettingsCheckboxes.tsx deleted file mode 100644 index aa92ce1b3..000000000 --- a/ts/components/conversation/SystemTraySettingsCheckboxes.tsx +++ /dev/null @@ -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 = ({ - i18n, - initialValue, - isSystemTraySupported, - onChange, -}) => { - const [localValue, setLocalValue] = useState( - parseSystemTraySetting(initialValue) - ); - - if (!isSystemTraySupported) { - return null; - } - - const setValue = (value: SystemTraySetting): void => { - setLocalValue(oldValue => { - if (oldValue !== value) { - onChange(value); - } - return value; - }); - }; - - const setMinimizeToSystemTray = (event: ChangeEvent) => { - setValue( - event.target.checked - ? SystemTraySetting.MinimizeToSystemTray - : SystemTraySetting.DoNotUseSystemTray - ); - }; - - const setMinimizeToAndStartInSystemTray = ( - event: ChangeEvent - ) => { - setValue( - event.target.checked - ? SystemTraySetting.MinimizeToAndStartInSystemTray - : SystemTraySetting.MinimizeToSystemTray - ); - }; - - const minimizesToTray = shouldMinimizeToSystemTray(localValue); - const minimizesToAndStartsInSystemTray = - localValue === SystemTraySetting.MinimizeToAndStartInSystemTray; - - return ( - <> -
- - {/* These manual spaces mirror the non-React parts of the settings screen. */}{' '} - -
-
- {' '} - {/* 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. */} - -
- - ); -}; diff --git a/ts/signal.ts b/ts/signal.ts index 758ec4a1b..52daad89b 100644 --- a/ts/signal.ts +++ b/ts/signal.ts @@ -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 = { diff --git a/ts/test-node/app/SystemTraySettingCache_test.ts b/ts/test-node/app/SystemTraySettingCache_test.ts index 94bd96a30..60e58f941 100644 --- a/ts/test-node/app/SystemTraySettingCache_test.ts +++ b/ts/test-node/app/SystemTraySettingCache_test.ts @@ -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 ) ); }); diff --git a/ts/test-node/types/SystemTraySetting_test.ts b/ts/test-node/types/SystemTraySetting_test.ts index b946da934..9bd6e2c0d 100644 --- a/ts/test-node/types/SystemTraySetting_test.ts +++ b/ts/test-node/types/SystemTraySetting_test.ts @@ -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 ); }); }); diff --git a/ts/types/Settings.ts b/ts/types/Settings.ts index 19b67cdc6..bd9e11062 100644 --- a/ts/types/Settings.ts +++ b/ts/types/Settings.ts @@ -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(); diff --git a/ts/types/SystemTraySetting.ts b/ts/types/SystemTraySetting.ts index 76a13af0d..eda1a435e 100644 --- a/ts/types/SystemTraySetting.ts +++ b/ts/types/SystemTraySetting.ts @@ -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 ); diff --git a/ts/windows/main/phase1-ipc.ts b/ts/windows/main/phase1-ipc.ts index e28e8a583..aeadb3105 100644 --- a/ts/windows/main/phase1-ipc.ts +++ b/ts/windows/main/phase1-ipc.ts @@ -154,7 +154,7 @@ window.setMenuBarVisibility = visibility => window.updateSystemTraySetting = ( systemTraySetting /* : Readonly */ ) => { - ipc.send('update-system-tray-setting', systemTraySetting); + ipc.invoke('update-system-tray-setting', systemTraySetting); }; window.restart = () => { diff --git a/ts/windows/settings/preload.ts b/ts/windows/settings/preload.ts index ef12d685a..4c65950d4 100644 --- a/ts/windows/settings/preload.ts +++ b/ts/windows/settings/preload.ts @@ -285,6 +285,10 @@ const renderPreferences = async () => { isSystemTraySupported: Settings.isSystemTraySupported( SignalContext.getVersion() ), + isMinimizeToAndStartInSystemTraySupported: + Settings.isMinimizeToAndStartInSystemTraySupported( + SignalContext.getVersion() + ), // Change handlers onAudioNotificationsChange: reRender(settingAudioNotification.setValue),