Use patched frameless-titlebar on Windows

This commit is contained in:
Fedor Indutny 2022-06-08 15:00:32 -07:00 committed by GitHub
parent 79c52847cd
commit 5634601554
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1343 additions and 323 deletions

View file

@ -3,6 +3,11 @@
<!-- prettier-ignore --> <!-- prettier-ignore -->
<link rel="stylesheet" href="../stylesheets/manifest.css" /> <link rel="stylesheet" href="../stylesheets/manifest.css" />
<link
href="../node_modules/@indutny/frameless-titlebar/dist/styles.css"
rel="stylesheet"
type="text/css"
/>
<script> <script>
window.SignalWindow = window.SignalWindow || {}; window.SignalWindow = window.SignalWindow || {};
window.SignalWindow.log = { window.SignalWindow.log = {
@ -13,4 +18,17 @@
debug: console.debug.bind(console), debug: console.debug.bind(console),
trace: console.trace.bind(console), trace: console.trace.bind(console),
}; };
window.SignalContext = {
nativeThemeListener: {
getSystemValue: async () => 'light',
subscribe: () => {},
unsubscribe: () => {},
},
Settings: {
themeSetting: {
getValue: async () => 'light',
},
waitForChange: () => {},
},
};
</script> </script>

View file

@ -5,6 +5,30 @@
Signal Desktop makes use of the following open source projects. Signal Desktop makes use of the following open source projects.
## @indutny/frameless-titlebar
MIT License
Copyright (c) 2019 Cristian Ponce
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## @popperjs/core ## @popperjs/core
License: MIT License: MIT

View file

@ -20,6 +20,11 @@
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
/> />
<link
href="node_modules/@indutny/frameless-titlebar/dist/styles.css"
rel="stylesheet"
type="text/css"
/>
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" /> <link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
</head> </head>
<body> <body>

View file

@ -27,6 +27,10 @@ import {
shell, shell,
systemPreferences, systemPreferences,
} from 'electron'; } from 'electron';
import type {
MenuItemConstructorOptions,
TitleBarOverlayOptions,
} from 'electron';
import { z } from 'zod'; import { z } from 'zod';
import packageJson from '../package.json'; import packageJson from '../package.json';
@ -75,7 +79,8 @@ import * as logging from '../ts/logging/main_process_logging';
import { MainSQL } from '../ts/sql/main'; import { MainSQL } from '../ts/sql/main';
import * as sqlChannels from './sql_channel'; import * as sqlChannels from './sql_channel';
import * as windowState from './window_state'; import * as windowState from './window_state';
import type { MenuOptionsType } from './menu'; import type { CreateTemplateOptionsType } from './menu';
import type { MenuActionType } from '../ts/types/menu';
import { createTemplate } from './menu'; import { createTemplate } from './menu';
import { installFileHandler, installWebHandler } from './protocol_filter'; import { installFileHandler, installWebHandler } from './protocol_filter';
import * as OS from '../ts/OS'; import * as OS from '../ts/OS';
@ -91,10 +96,6 @@ import {
} from '../ts/util/sgnlHref'; } from '../ts/util/sgnlHref';
import { clearTimeoutIfNecessary } from '../ts/util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from '../ts/util/clearTimeoutIfNecessary';
import { toggleMaximizedBrowserWindow } from '../ts/util/toggleMaximizedBrowserWindow'; import { toggleMaximizedBrowserWindow } from '../ts/util/toggleMaximizedBrowserWindow';
import {
getTitleBarVisibility,
TitleBarVisibility,
} from '../ts/types/Settings';
import { ChallengeMainHandler } from '../ts/main/challengeMain'; import { ChallengeMainHandler } from '../ts/main/challengeMain';
import { NativeThemeNotifier } from '../ts/main/NativeThemeNotifier'; import { NativeThemeNotifier } from '../ts/main/NativeThemeNotifier';
import { PowerChannel } from '../ts/main/powerChannel'; import { PowerChannel } from '../ts/main/powerChannel';
@ -324,6 +325,8 @@ if (windowFromUserConfig) {
ephemeralConfig.set('window', windowConfig); ephemeralConfig.set('window', windowConfig);
} }
let menuOptions: CreateTemplateOptionsType | undefined;
// These will be set after app fires the 'ready' event // These will be set after app fires the 'ready' event
let logger: LoggerType | undefined; let logger: LoggerType | undefined;
let locale: LocaleType | undefined; let locale: LocaleType | undefined;
@ -429,7 +432,10 @@ async function handleUrl(event: Electron.Event, rawTarget: string) {
} }
} }
function handleCommonWindowEvents(window: BrowserWindow) { function handleCommonWindowEvents(
window: BrowserWindow,
titleBarOverlay: TitleBarOverlayOptions | false = false
) {
window.webContents.on('will-navigate', handleUrl); window.webContents.on('will-navigate', handleUrl);
window.webContents.on('new-window', handleUrl); window.webContents.on('new-window', handleUrl);
window.webContents.on( window.webContents.on(
@ -467,6 +473,23 @@ function handleCommonWindowEvents(window: BrowserWindow) {
window.webContents.on('preferred-size-changed', onZoomChanged); window.webContents.on('preferred-size-changed', onZoomChanged);
nativeThemeNotifier.addWindow(window); nativeThemeNotifier.addWindow(window);
if (titleBarOverlay) {
const onThemeChange = async () => {
try {
const newOverlay = await getTitleBarOverlay();
if (!newOverlay) {
return;
}
window.setTitleBarOverlay(newOverlay);
} catch (error) {
console.error('onThemeChange error', error);
}
};
nativeTheme.on('updated', onThemeChange);
settingsChannel?.on('change:themeSetting', onThemeChange);
}
} }
const DEFAULT_WIDTH = 800; const DEFAULT_WIDTH = 800;
@ -521,10 +544,50 @@ if (OS.isWindows()) {
windowIcon = join(__dirname, '../build/icons/png/512x512.png'); windowIcon = join(__dirname, '../build/icons/png/512x512.png');
} }
const mainTitleBarStyle =
OS.isLinux() || isTestEnvironment(getEnvironment())
? ('default' as const)
: ('hidden' as const);
const nonMainTitleBarStyle = OS.isWindows()
? ('hidden' as const)
: ('default' as const);
async function getTitleBarOverlay(): Promise<TitleBarOverlayOptions | false> {
if (!OS.isWindows()) {
return false;
}
const theme = await getResolvedThemeSetting();
let color: string;
let symbolColor: string;
if (theme === 'light') {
color = '#e8e8e8';
symbolColor = '#1b1b1b';
} else if (theme === 'dark') {
color = '#24292e';
symbolColor = '#fff';
} else {
throw missingCaseError(theme);
}
return {
color,
symbolColor,
// Should match stylesheets/components/TitleBarContainer.scss minus the
// border
height: 28 - 1,
};
}
async function createWindow() { async function createWindow() {
const usePreloadBundle = const usePreloadBundle =
!isTestEnvironment(getEnvironment()) || forcePreloadBundle; !isTestEnvironment(getEnvironment()) || forcePreloadBundle;
const titleBarOverlay = await getTitleBarOverlay();
const windowOptions: Electron.BrowserWindowConstructorOptions = { const windowOptions: Electron.BrowserWindowConstructorOptions = {
show: false, show: false,
width: DEFAULT_WIDTH, width: DEFAULT_WIDTH,
@ -532,11 +595,8 @@ async function createWindow() {
minWidth: MIN_WIDTH, minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT, minHeight: MIN_HEIGHT,
autoHideMenuBar: false, autoHideMenuBar: false,
titleBarStyle: titleBarStyle: mainTitleBarStyle,
getTitleBarVisibility() === TitleBarVisibility.Hidden && titleBarOverlay,
!isTestEnvironment(getEnvironment())
? 'hidden'
: 'default',
backgroundColor: isTestEnvironment(getEnvironment()) backgroundColor: isTestEnvironment(getEnvironment())
? '#ffffff' // Tests should always be rendered on a white background ? '#ffffff' // Tests should always be rendered on a white background
: await getBackgroundColor(), : await getBackgroundColor(),
@ -616,7 +676,20 @@ async function createWindow() {
systemTrayService.setMainWindow(mainWindow); systemTrayService.setMainWindow(mainWindow);
} }
function captureAndSaveWindowStats() { function saveWindowStats() {
if (!windowConfig) {
return;
}
getLogger().info(
'Updating BrowserWindow config: %s',
JSON.stringify(windowConfig)
);
ephemeralConfig.set('window', windowConfig);
}
const debouncedSaveStats = debounce(saveWindowStats, 500);
function captureWindowStats() {
if (!mainWindow) { if (!mainWindow) {
return; return;
} }
@ -624,8 +697,7 @@ async function createWindow() {
const size = mainWindow.getSize(); const size = mainWindow.getSize();
const position = mainWindow.getPosition(); const position = mainWindow.getPosition();
// so if we need to recreate the window, we have the most recent settings const newWindowConfig = {
windowConfig = {
maximized: mainWindow.isMaximized(), maximized: mainWindow.isMaximized(),
autoHideMenuBar: mainWindow.autoHideMenuBar, autoHideMenuBar: mainWindow.autoHideMenuBar,
fullscreen: mainWindow.isFullScreen(), fullscreen: mainWindow.isFullScreen(),
@ -635,16 +707,24 @@ async function createWindow() {
y: position[1], y: position[1],
}; };
getLogger().info( if (
'Updating BrowserWindow config: %s', newWindowConfig.fullscreen !== windowConfig?.fullscreen ||
JSON.stringify(windowConfig) newWindowConfig.maximized !== windowConfig?.maximized
); ) {
ephemeralConfig.set('window', windowConfig); mainWindow.webContents.send('window:set-window-stats', {
isMaximized: newWindowConfig.maximized,
isFullScreen: newWindowConfig.fullscreen,
});
}
// so if we need to recreate the window, we have the most recent settings
windowConfig = newWindowConfig;
debouncedSaveStats();
} }
const debouncedCaptureStats = debounce(captureAndSaveWindowStats, 500); mainWindow.on('resize', captureWindowStats);
mainWindow.on('resize', debouncedCaptureStats); mainWindow.on('move', captureWindowStats);
mainWindow.on('move', debouncedCaptureStats);
const setWindowFocus = () => { const setWindowFocus = () => {
if (!mainWindow) { if (!mainWindow) {
@ -681,7 +761,7 @@ async function createWindow() {
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} }
handleCommonWindowEvents(mainWindow); handleCommonWindowEvents(mainWindow, titleBarOverlay);
// App dock icon bounce // App dock icon bounce
bounce.init(mainWindow); bounce.init(mainWindow);
@ -981,6 +1061,7 @@ function showScreenShareWindow(sourceName: string) {
resizable: false, resizable: false,
show: false, show: false,
title: getLocale().i18n('screenShareWindow'), title: getLocale().i18n('screenShareWindow'),
titleBarStyle: nonMainTitleBarStyle,
width, width,
webPreferences: { webPreferences: {
...defaultWebPrefs, ...defaultWebPrefs,
@ -1021,11 +1102,15 @@ async function showAbout() {
return; return;
} }
const titleBarOverlay = await getTitleBarOverlay();
const options = { const options = {
width: 500, width: 500,
height: 500, height: 500,
resizable: false, resizable: false,
title: getLocale().i18n('aboutSignalDesktop'), title: getLocale().i18n('aboutSignalDesktop'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true, autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(), backgroundColor: await getBackgroundColor(),
show: false, show: false,
@ -1041,7 +1126,7 @@ async function showAbout() {
aboutWindow = new BrowserWindow(options); aboutWindow = new BrowserWindow(options);
handleCommonWindowEvents(aboutWindow); handleCommonWindowEvents(aboutWindow, titleBarOverlay);
aboutWindow.loadURL(prepareFileUrl([__dirname, '../about.html'])); aboutWindow.loadURL(prepareFileUrl([__dirname, '../about.html']));
@ -1063,12 +1148,16 @@ async function showSettingsWindow() {
return; return;
} }
const titleBarOverlay = await getTitleBarOverlay();
const options = { const options = {
width: 700, width: 700,
height: 700, height: 700,
frame: true, frame: true,
resizable: false, resizable: false,
title: getLocale().i18n('signalDesktopPreferences'), title: getLocale().i18n('signalDesktopPreferences'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true, autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(), backgroundColor: await getBackgroundColor(),
show: false, show: false,
@ -1084,7 +1173,7 @@ async function showSettingsWindow() {
settingsWindow = new BrowserWindow(options); settingsWindow = new BrowserWindow(options);
handleCommonWindowEvents(settingsWindow); handleCommonWindowEvents(settingsWindow, titleBarOverlay);
settingsWindow.loadURL(prepareFileUrl([__dirname, '../settings.html'])); settingsWindow.loadURL(prepareFileUrl([__dirname, '../settings.html']));
@ -1132,6 +1221,7 @@ async function showStickerCreator() {
const { x = 0, y = 0 } = windowConfig || {}; const { x = 0, y = 0 } = windowConfig || {};
// TODO: DESKTOP-3670
const options = { const options = {
x: x + 100, x: x + 100,
y: y + 100, y: y + 100,
@ -1191,12 +1281,16 @@ async function showDebugLogWindow() {
return; return;
} }
const titleBarOverlay = await getTitleBarOverlay();
const theme = await getThemeSetting(); const theme = await getThemeSetting();
const options = { const options = {
width: 700, width: 700,
height: 500, height: 500,
resizable: false, resizable: false,
title: getLocale().i18n('debugLog'), title: getLocale().i18n('debugLog'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true, autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(), backgroundColor: await getBackgroundColor(),
show: false, show: false,
@ -1218,7 +1312,7 @@ async function showDebugLogWindow() {
debugLogWindow = new BrowserWindow(options); debugLogWindow = new BrowserWindow(options);
handleCommonWindowEvents(debugLogWindow); handleCommonWindowEvents(debugLogWindow, titleBarOverlay);
debugLogWindow.loadURL( debugLogWindow.loadURL(
prepareFileUrl([__dirname, '../debug_log.html'], { theme }) prepareFileUrl([__dirname, '../debug_log.html'], { theme })
@ -1259,6 +1353,7 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) {
height: Math.min(150, size[1]), height: Math.min(150, size[1]),
resizable: false, resizable: false,
title: getLocale().i18n('allowAccess'), title: getLocale().i18n('allowAccess'),
titleBarStyle: nonMainTitleBarStyle,
autoHideMenuBar: true, autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(), backgroundColor: await getBackgroundColor(),
show: false, show: false,
@ -1681,9 +1776,9 @@ app.on('ready', async () => {
]); ]);
}); });
function setupMenu(options?: Partial<MenuOptionsType>) { function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
const { platform } = process; const { platform } = process;
const menuOptions = { menuOptions = {
// options // options
development, development,
devTools: defaultWebPrefs.devTools, devTools: defaultWebPrefs.devTools,
@ -1713,6 +1808,14 @@ function setupMenu(options?: Partial<MenuOptionsType>) {
const template = createTemplate(menuOptions, getLocale().messages); const template = createTemplate(menuOptions, getLocale().messages);
const menu = Menu.buildFromTemplate(template); const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
mainWindow?.webContents.send('window:set-menu-options', {
development: menuOptions.development,
devTools: menuOptions.devTools,
includeSetup: menuOptions.includeSetup,
isProduction: menuOptions.isProduction,
platform: menuOptions.platform,
});
} }
async function requestShutdown() { async function requestShutdown() {
@ -1910,12 +2013,6 @@ ipc.on(
} }
); );
ipc.on('close-about', () => {
if (aboutWindow) {
aboutWindow.close();
}
});
ipc.on('close-screen-share-controller', () => { ipc.on('close-screen-share-controller', () => {
if (screenShareWindow) { if (screenShareWindow) {
screenShareWindow.close(); screenShareWindow.close();
@ -1941,11 +2038,6 @@ ipc.on('update-tray-icon', (_event: Electron.Event, unreadCount: number) => {
// Debug Log-related IPC calls // Debug Log-related IPC calls
ipc.on('show-debug-log', showDebugLogWindow); ipc.on('show-debug-log', showDebugLogWindow);
ipc.on('close-debug-log', () => {
if (debugLogWindow) {
debugLogWindow.close();
}
});
ipc.on( ipc.on(
'show-debug-log-save-dialog', 'show-debug-log-save-dialog',
async (_event: Electron.Event, logText: string) => { async (_event: Electron.Event, logText: string) => {
@ -1973,11 +2065,6 @@ ipc.handle(
} }
} }
); );
ipc.on('close-permissions-popup', () => {
if (permissionsPopupWindow) {
permissionsPopupWindow.close();
}
});
// Settings-related IPC calls // Settings-related IPC calls
@ -1993,11 +2080,6 @@ function removeDarkOverlay() {
} }
ipc.on('show-settings', showSettingsWindow); ipc.on('show-settings', showSettingsWindow);
ipc.on('close-settings', () => {
if (settingsWindow) {
settingsWindow.close();
}
});
ipc.on('delete-all-data', () => { ipc.on('delete-all-data', () => {
if (settingsWindow) { if (settingsWindow) {
@ -2188,6 +2270,124 @@ ipc.handle('getScreenCaptureSources', async () => {
}); });
}); });
ipc.handle('executeMenuRole', async ({ sender }, untypedRole) => {
const role = untypedRole as MenuItemConstructorOptions['role'];
const senderWindow = BrowserWindow.fromWebContents(sender);
switch (role) {
case 'undo':
sender.undo();
break;
case 'redo':
sender.redo();
break;
case 'cut':
sender.cut();
break;
case 'copy':
sender.copy();
break;
case 'paste':
sender.paste();
break;
case 'pasteAndMatchStyle':
sender.pasteAndMatchStyle();
break;
case 'delete':
sender.delete();
break;
case 'selectAll':
sender.selectAll();
break;
case 'reload':
sender.reload();
break;
case 'toggleDevTools':
sender.toggleDevTools();
break;
case 'resetZoom':
sender.setZoomLevel(0);
break;
case 'zoomIn':
sender.setZoomLevel(sender.getZoomLevel() + 1);
break;
case 'zoomOut':
sender.setZoomLevel(sender.getZoomLevel() - 1);
break;
case 'togglefullscreen':
senderWindow?.setFullScreen(!senderWindow?.isFullScreen());
break;
case 'minimize':
senderWindow?.minimize();
break;
case 'close':
senderWindow?.close();
break;
case 'quit':
app.quit();
break;
default:
// ignored
break;
}
});
ipc.handle('getMainWindowStats', async () => {
return {
isMaximized: windowConfig?.maximized ?? false,
isFullScreen: windowConfig?.fullscreen ?? false,
};
});
ipc.handle('getMenuOptions', async () => {
return {
development: menuOptions?.development ?? false,
devTools: menuOptions?.devTools ?? false,
includeSetup: menuOptions?.includeSetup ?? false,
isProduction: menuOptions?.isProduction ?? true,
platform: menuOptions?.platform ?? 'unknown',
};
});
ipc.handle('executeMenuAction', async (_event, action: MenuActionType) => {
if (action === 'forceUpdate') {
forceUpdate();
} else if (action === 'openContactUs') {
openContactUs();
} else if (action === 'openForums') {
openForums();
} else if (action === 'openJoinTheBeta') {
openJoinTheBeta();
} else if (action === 'openReleaseNotes') {
openReleaseNotes();
} else if (action === 'openSupportPage') {
openSupportPage();
} else if (action === 'setupAsNewDevice') {
setupAsNewDevice();
} else if (action === 'setupAsStandalone') {
setupAsStandalone();
} else if (action === 'showAbout') {
showAbout();
} else if (action === 'showDebugLog') {
showDebugLogWindow();
} else if (action === 'showKeyboardShortcuts') {
showKeyboardShortcuts();
} else if (action === 'showSettings') {
showSettingsWindow();
} else if (action === 'showStickerCreator') {
showStickerCreator();
} else if (action === 'showWindow') {
showWindow();
} else {
throw missingCaseError(action);
}
});
if (isTestEnvironment(getEnvironment())) { if (isTestEnvironment(getEnvironment())) {
ipc.handle('ci:test-electron:done', async (_event, info) => { ipc.handle('ci:test-electron:done', async (_event, info) => {
if (!process.env.TEST_QUIT_ON_COMPLETE) { if (!process.env.TEST_QUIT_ON_COMPLETE) {

View file

@ -1,40 +1,19 @@
// Copyright 2017-2020 Signal Messenger, LLC // Copyright 2017-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isString } from 'lodash'; import { isString } from 'lodash';
import type { MenuItemConstructorOptions } from 'electron';
import type { LocaleMessagesType } from '../ts/types/I18N'; import type { LocaleMessagesType } from '../ts/types/I18N';
import type {
MenuListType,
MenuOptionsType,
MenuActionsType,
} from '../ts/types/menu';
export type MenuListType = Array<MenuItemConstructorOptions>; export type CreateTemplateOptionsType = MenuOptionsType & MenuActionsType;
export type MenuOptionsType = {
// options
development: boolean;
devTools: boolean;
includeSetup: boolean;
isProduction: boolean;
platform: string;
// actions
forceUpdate: () => unknown;
openContactUs: () => unknown;
openForums: () => unknown;
openJoinTheBeta: () => unknown;
openReleaseNotes: () => unknown;
openSupportPage: () => unknown;
setupAsNewDevice: () => unknown;
setupAsStandalone: () => unknown;
showAbout: () => unknown;
showDebugLog: () => unknown;
showKeyboardShortcuts: () => unknown;
showSettings: () => unknown;
showStickerCreator: () => unknown;
showWindow: () => unknown;
};
export const createTemplate = ( export const createTemplate = (
options: MenuOptionsType, options: CreateTemplateOptionsType,
messages: LocaleMessagesType messages: LocaleMessagesType
): MenuListType => { ): MenuListType => {
if (!isString(options.platform)) { if (!isString(options.platform)) {
@ -131,7 +110,7 @@ export const createTemplate = (
label: messages.viewMenuResetZoom.message, label: messages.viewMenuResetZoom.message,
}, },
{ {
accelerator: platform === 'darwin' ? 'Command+=' : 'Control+=', accelerator: 'CmdOrCtrl+=',
role: 'zoomIn', role: 'zoomIn',
label: messages.viewMenuZoomIn.message, label: messages.viewMenuZoomIn.message,
}, },
@ -265,7 +244,7 @@ export const createTemplate = (
function updateForMac( function updateForMac(
template: MenuListType, template: MenuListType,
messages: LocaleMessagesType, messages: LocaleMessagesType,
options: MenuOptionsType options: CreateTemplateOptionsType
): MenuListType { ): MenuListType {
const { showAbout, showSettings, showWindow } = options; const { showAbout, showSettings, showWindow } = options;

View file

@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
import { maybeParseUrl } from '../ts/util/url'; import { maybeParseUrl } from '../ts/util/url';
import type { LocaleType } from './locale'; import type { LocaleType } from './locale';
import type { MenuListType } from './menu'; import type { MenuListType } from '../ts/types/menu';
export function getLanguages( export function getLanguages(
userLocale: string, userLocale: string,

View file

@ -23,7 +23,7 @@
img-src 'self' blob: data:; img-src 'self' blob: data:;
media-src 'self' blob:; media-src 'self' blob:;
object-src 'none'; object-src 'none';
script-src 'self' 'sha256-eLeGwSfPmXJ+EUiLfIeXABvLiUqDbiKgNLpHITaabgQ='; script-src 'self' 'sha256-Qu05oqDmBO5fZacm7tr/oerJcqsW0G/XqP4PRCziovc=' 'sha256-eLeGwSfPmXJ+EUiLfIeXABvLiUqDbiKgNLpHITaabgQ=';
style-src 'self' 'unsafe-inline';" style-src 'self' 'unsafe-inline';"
/> />
<title>Signal</title> <title>Signal</title>
@ -81,6 +81,11 @@
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
/> />
<link
href="node_modules/@indutny/frameless-titlebar/dist/styles.css"
rel="stylesheet"
type="text/css"
/>
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" /> <link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
<!-- <!--
@ -152,6 +157,12 @@
</div> </div>
</div> </div>
<script type="text/javascript">
document
.querySelector('.app-loading-screen')
.addEventListener('dblclick', () => window.showDebugLog());
</script>
<script type="text/javascript" src="js/components.js"></script> <script type="text/javascript" src="js/components.js"></script>
<script type="text/javascript" src="ts/set_os_class.js"></script> <script type="text/javascript" src="ts/set_os_class.js"></script>
<script <script
@ -164,13 +175,6 @@
src="ts/backbone/reliable_trigger.js" src="ts/backbone/reliable_trigger.js"
></script> ></script>
<script type="text/javascript" src="js/libphonenumber-util.js"></script>
<script type="text/javascript" src="js/expiring_messages.js"></script>
<script
type="text/javascript"
src="js/expiring_tap_to_view_messages.js"
></script>
<script <script
type="text/javascript" type="text/javascript"
src="js/views/react_wrapper_view.js" src="js/views/react_wrapper_view.js"

View file

@ -20,6 +20,11 @@
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
/> />
<link
href="node_modules/@indutny/frameless-titlebar/dist/styles.css"
rel="stylesheet"
type="text/css"
/>
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" /> <link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
</head> </head>
<body> <body>

View file

@ -76,6 +76,7 @@
"fs-xattr": "0.3.0" "fs-xattr": "0.3.0"
}, },
"dependencies": { "dependencies": {
"@indutny/frameless-titlebar": "2.1.4-rc.8",
"@popperjs/core": "2.9.2", "@popperjs/core": "2.9.2",
"@react-spring/web": "9.4.5", "@react-spring/web": "9.4.5",
"@signalapp/libsignal-client": "0.16.0", "@signalapp/libsignal-client": "0.16.0",

View file

@ -233,6 +233,20 @@ try {
Whisper.events.trigger('powerMonitorLockScreen'); Whisper.events.trigger('powerMonitorLockScreen');
}); });
ipc.on('window:set-window-stats', (_event, stats) => {
if (!Whisper.events) {
return;
}
Whisper.events.trigger('setWindowStats', stats);
});
ipc.on('window:set-menu-options', (_event, options) => {
if (!Whisper.events) {
return;
}
Whisper.events.trigger('setMenuOptions', options);
});
window.sendChallengeRequest = request => window.sendChallengeRequest = request =>
ipc.send('challenge:request', request); ipc.send('challenge:request', request);

View file

@ -20,6 +20,11 @@
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
/> />
<link
href="node_modules/@indutny/frameless-titlebar/dist/styles.css"
rel="stylesheet"
type="text/css"
/>
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" /> <link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
</head> </head>
<body> <body>

View file

@ -4,7 +4,7 @@
.facade { .facade {
background: rgba(0, 0, 0, 0.33); background: rgba(0, 0, 0, 0.33);
width: 100vw; width: 100vw;
height: 100vh; height: var(--window-height);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View file

@ -4,6 +4,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link
href="../../node_modules/@indutny/frameless-titlebar/src/title-bar/style.css"
rel="stylesheet"
type="text/css"
/>
<link rel="stylesheet" href="../../stylesheets/manifest_bridge.css" /> <link rel="stylesheet" href="../../stylesheets/manifest_bridge.css" />
</head> </head>
<body> <body>

View file

@ -254,7 +254,7 @@ const getThemeSetting = createSetting('themeSetting');
async function resolveTheme() { async function resolveTheme() {
const theme = (await getThemeSetting.getValue()) || 'system'; const theme = (await getThemeSetting.getValue()) || 'system';
if (process.platform === 'darwin' && theme === 'system') { if (theme === 'system') {
return SignalContext.nativeThemeListener.getSystemTheme(); return SignalContext.nativeThemeListener.getSystemTheme();
} }
return theme; return theme;

View file

@ -27,6 +27,14 @@ body {
--draggable-app-region: drag; --draggable-app-region: drag;
} }
--window-height: 100vh;
--titlebar-height: 0px;
&.os-windows:not(.full-screen) {
--titlebar-height: 28px;
--window-height: calc(100vh - var(--titlebar-height));
}
&.light-theme { &.light-theme {
background-color: $color-white; background-color: $color-white;
color: $color-gray-90; color: $color-gray-90;
@ -236,6 +244,9 @@ $loading-height: 16px;
top: 0; top: 0;
bottom: 0; bottom: 0;
/* There is no titlebar during loading screen on Windows */
-webkit-app-region: drag;
/* Note: background-color is intentionally transparent until body has the /* Note: background-color is intentionally transparent until body has the
* theme class. * theme class.
*/ */

View file

@ -672,7 +672,7 @@
@mixin install-screen { @mixin install-screen {
align-items: center; align-items: center;
display: flex; display: flex;
height: 100vh; height: var(--window-height);
justify-content: center; justify-content: center;
line-height: 30px; line-height: 30px;
user-select: none; user-select: none;

View file

@ -3741,7 +3741,7 @@ button.module-image__border-overlay:focus {
background-color: $calling-background-color; background-color: $calling-background-color;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: var(--window-height);
justify-content: center; justify-content: center;
position: absolute; position: absolute;
width: 100%; width: 100%;
@ -3855,7 +3855,7 @@ button.module-image__border-overlay:focus {
&__remote-video-disabled { &__remote-video-disabled {
background-color: $color-gray-95; background-color: $color-gray-95;
height: 100vh; height: var(--window-height);
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
@ -4292,7 +4292,7 @@ button.module-image__border-overlay:focus {
&__overlay { &__overlay {
display: flex; display: flex;
height: 100vh; height: var(--window-height);
justify-content: flex-end; justify-content: flex-end;
left: 0; left: 0;
position: absolute; position: absolute;
@ -4415,7 +4415,7 @@ button.module-image__border-overlay:focus {
color: $color-gray-05; color: $color-gray-05;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: var(--window-height);
justify-content: center; justify-content: center;
position: relative; position: relative;
width: 100%; width: 100%;
@ -5983,7 +5983,7 @@ button.module-image__border-overlay:focus {
left: 0; left: 0;
top: 0; top: 0;
width: 100vw; width: 100vw;
height: 100vh; height: var(--window-height);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -7281,7 +7281,7 @@ button.module-image__border-overlay:focus {
border-radius: 4px; border-radius: 4px;
padding: 16px; padding: 16px;
max-height: calc(100vh - 40px); max-height: calc(var(--window-height) - 40px);
max-width: 1150px; max-width: 1150px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@ -7626,7 +7626,7 @@ button.module-image__border-overlay:focus {
.module-modal-host__overlay { .module-modal-host__overlay {
background: $color-black-alpha-40; background: $color-black-alpha-40;
height: 100vh; height: var(--window-height);
left: 0; left: 0;
position: absolute; position: absolute;
top: 0; top: 0;
@ -7637,7 +7637,7 @@ button.module-image__border-overlay:focus {
.module-modal-host__overlay-container { .module-modal-host__overlay-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: var(--window-height);
justify-content: center; justify-content: center;
left: 0; left: 0;
overflow: hidden; overflow: hidden;
@ -7784,7 +7784,7 @@ button.module-image__border-overlay:focus {
left: 0; left: 0;
top: 0; top: 0;
width: 100vw; width: 100vw;
height: 100vh; height: var(--window-height);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View file

@ -257,6 +257,7 @@ $z-index-context-menu: 125;
$z-index-tooltip: 150; $z-index-tooltip: 150;
$z-index-toast: 200; $z-index-toast: 200;
$z-index-on-top-of-everything: 9000; $z-index-on-top-of-everything: 9000;
$z-index-window-controls: 10000;
// Component specific // Component specific
// The scroll down button should be above everything in the timeline but // The scroll down button should be above everything in the timeline but

View file

@ -7,7 +7,7 @@
color: $color-white; color: $color-white;
display: flex; display: flex;
font-size: 14px; font-size: 14px;
height: 100vh; height: var(--window-height);
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;

View file

@ -5,6 +5,10 @@
height: 100%; height: 100%;
position: relative; position: relative;
// TitleBar support
display: flex;
flex-direction: column;
&.light-theme { &.light-theme {
background-color: $color-white; background-color: $color-white;
color: $color-gray-90; color: $color-gray-90;

View file

@ -8,7 +8,7 @@
left: 0; left: 0;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: var(--titlebar-height);
z-index: $z-index-popup; z-index: $z-index-popup;
} }

View file

@ -7,10 +7,10 @@
background: $color-gray-95; background: $color-gray-95;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: var(--window-height);
left: 0; left: 0;
position: absolute; position: absolute;
top: 0; top: var(--titlebar-height);
user-select: none; user-select: none;
width: 100vw; width: 100vw;
z-index: $z-index-popup-overlay; z-index: $z-index-popup-overlay;

View file

@ -120,7 +120,7 @@
} }
&__settings-pane { &__settings-pane {
height: 100vh; height: var(--window-height);
overflow: overlay; overflow: overlay;
width: 100%; width: 100%;

View file

@ -4,10 +4,10 @@
.Stories { .Stories {
background: $color-gray-95; background: $color-gray-95;
display: flex; display: flex;
height: 100vh; height: var(--window-height);
left: 0; left: 0;
position: absolute; position: absolute;
top: 0; top: var(--titlebar-height);
user-select: none; user-select: none;
width: 100%; width: 100%;
z-index: $z-index-stories; z-index: $z-index-stories;

View file

@ -4,10 +4,10 @@
.StoryViewer { .StoryViewer {
&__overlay { &__overlay {
background-size: contain; background-size: contain;
height: 100vh; height: var(--window-height);
left: 0; left: 0;
position: absolute; position: absolute;
top: 0; top: var(--titlebar-height);
width: 100%; width: 100%;
z-index: $z-index-popup-overlay; z-index: $z-index-popup-overlay;
} }
@ -18,11 +18,11 @@
background: $color-black-alpha-20; background: $color-black-alpha-20;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: var(--window-height);
justify-content: center; justify-content: center;
left: 0; left: 0;
position: absolute; position: absolute;
top: 0; top: var(--titlebar-height);
width: 100%; width: 100%;
z-index: $z-index-popup-overlay; z-index: $z-index-popup-overlay;
} }
@ -238,7 +238,7 @@
} }
&__animated-emojis { &__animated-emojis {
height: 100vh; height: var(--window-height);
position: absolute; position: absolute;
width: 100%; width: 100%;
z-index: $z-index-above-base; z-index: $z-index-above-base;
@ -248,7 +248,7 @@
@include button-reset; @include button-reset;
align-items: center; align-items: center;
display: flex; display: flex;
height: 100vh; height: var(--window-height);
position: absolute; position: absolute;
width: 25%; width: 25%;
z-index: $z-index-above-above-base; z-index: $z-index-above-above-base;

View file

@ -0,0 +1,31 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.TitleBarContainer {
display: flex;
flex-direction: column;
height: 100vh;
&__title {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: $z-index-window-controls;
// This matches the inline styles of frameless-titlebar
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Arial,
sans-serif;
& button {
font-family: inherit;
}
}
&__content {
margin-top: var(--titlebar-height);
height: var(--window-height);
position: relative;
}
}

View file

@ -114,5 +114,6 @@
@import './components/TimelineFloatingHeader.scss'; @import './components/TimelineFloatingHeader.scss';
@import './components/TimelineWarning.scss'; @import './components/TimelineWarning.scss';
@import './components/TimelineWarnings.scss'; @import './components/TimelineWarnings.scss';
@import './components/TitleBarContainer.scss';
@import './components/Toast.scss'; @import './components/Toast.scss';
@import './components/WhatsNew.scss'; @import './components/WhatsNew.scss';

View file

@ -26,8 +26,8 @@ import * as Bytes from './Bytes';
import * as Timers from './Timers'; import * as Timers from './Timers';
import * as indexedDb from './indexeddb'; import * as indexedDb from './indexeddb';
import type { WhatIsThis } from './window.d'; import type { WhatIsThis } from './window.d';
import type { MenuOptionsType } from './types/menu';
import type { Receipt } from './types/Receipt'; import type { Receipt } from './types/Receipt';
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
import { SocketStatus } from './types/SocketStatus'; import { SocketStatus } from './types/SocketStatus';
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors'; import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
import { ThemeType } from './types/Util'; import { ThemeType } from './types/Util';
@ -141,6 +141,7 @@ import { ToastConversationArchived } from './components/ToastConversationArchive
import { ToastConversationUnarchived } from './components/ToastConversationUnarchived'; import { ToastConversationUnarchived } from './components/ToastConversationUnarchived';
import { showToast } from './util/showToast'; import { showToast } from './util/showToast';
import { startInteractionMode } from './windows/startInteractionMode'; import { startInteractionMode } from './windows/startInteractionMode';
import type { MainWindowStatsType } from './windows/context';
import { deliveryReceiptsJobQueue } from './jobs/deliveryReceiptsJobQueue'; import { deliveryReceiptsJobQueue } from './jobs/deliveryReceiptsJobQueue';
import { updateOurUsername } from './util/updateOurUsername'; import { updateOurUsername } from './util/updateOurUsername';
import { ReactionSource } from './reactions/ReactionSource'; import { ReactionSource } from './reactions/ReactionSource';
@ -452,7 +453,7 @@ export async function startApp(): Promise<void> {
}, },
}); });
if (getTitleBarVisibility() === TitleBarVisibility.Hidden) { if (window.platform === 'darwin') {
window.addEventListener('dblclick', (event: Event) => { window.addEventListener('dblclick', (event: Event) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (isWindowDragElement(target)) { if (isWindowDragElement(target)) {
@ -930,6 +931,19 @@ export async function startApp(): Promise<void> {
} }
}, FIVE_MINUTES); }, FIVE_MINUTES);
let mainWindowStats = {
isMaximized: false,
isFullScreen: false,
};
let menuOptions = {
development: false,
devTools: false,
includeSetup: false,
isProduction: true,
platform: 'unknown',
};
try { try {
await Promise.all([ await Promise.all([
window.ConversationController.load(), window.ConversationController.load(),
@ -938,6 +952,12 @@ export async function startApp(): Promise<void> {
loadInitialBadgesState(), loadInitialBadgesState(),
loadStories(), loadStories(),
window.textsecure.storage.protocol.hydrateCaches(), window.textsecure.storage.protocol.hydrateCaches(),
(async () => {
mainWindowStats = await window.SignalContext.getMainWindowStats();
})(),
(async () => {
menuOptions = await window.SignalContext.getMenuOptions();
})(),
]); ]);
await window.ConversationController.checkForConflicts(); await window.ConversationController.checkForConflicts();
} catch (error) { } catch (error) {
@ -946,7 +966,7 @@ export async function startApp(): Promise<void> {
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} finally { } finally {
initializeRedux(); initializeRedux({ mainWindowStats, menuOptions });
start(); start();
window.Signal.Services.initializeNetworkObserver( window.Signal.Services.initializeNetworkObserver(
window.reduxActions.network window.reduxActions.network
@ -964,12 +984,20 @@ export async function startApp(): Promise<void> {
} }
}); });
function initializeRedux() { function initializeRedux({
mainWindowStats,
menuOptions,
}: {
mainWindowStats: MainWindowStatsType;
menuOptions: MenuOptionsType;
}) {
// Here we set up a full redux store with initial state for our LeftPane Root // Here we set up a full redux store with initial state for our LeftPane Root
const convoCollection = window.getConversations(); const convoCollection = window.getConversations();
const initialState = getInitialState({ const initialState = getInitialState({
badges: initialBadgesState, badges: initialBadgesState,
stories: getStoriesForRedux(), stories: getStoriesForRedux(),
mainWindowStats,
menuOptions,
}); });
const store = window.Signal.State.createStore(initialState); const store = window.Signal.State.createStore(initialState);
@ -1110,6 +1138,26 @@ export async function startApp(): Promise<void> {
} }
}); });
window.Whisper.events.on(
'setWindowStats',
({
isFullScreen,
isMaximized,
}: {
isFullScreen: boolean;
isMaximized: boolean;
}) => {
window.reduxActions.user.userChanged({
isMainWindowMaximized: isMaximized,
isMainWindowFullScreen: isFullScreen,
});
}
);
window.Whisper.events.on('setMenuOptions', (options: MenuOptionsType) => {
window.reduxActions.user.userChanged({ menuOptions: options });
});
let shortcutGuideView: WhatIsThis | null = null; let shortcutGuideView: WhatIsThis | null = null;
window.showKeyboardShortcuts = () => { window.showKeyboardShortcuts = () => {

View file

@ -1,15 +1,21 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useTheme } from '../hooks/useTheme';
import { TitleBarContainer } from './TitleBarContainer';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
export type PropsType = { export type PropsType = {
closeAbout: () => unknown; closeAbout: () => unknown;
environment: string; environment: string;
i18n: LocalizerType; i18n: LocalizerType;
version: string; version: string;
platform: string;
executeMenuRole: ExecuteMenuRoleType;
}; };
export const About = ({ export const About = ({
@ -17,34 +23,45 @@ export const About = ({
i18n, i18n,
environment, environment,
version, version,
platform,
executeMenuRole,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
useEscapeHandling(closeAbout); useEscapeHandling(closeAbout);
return ( const theme = useTheme();
<div className="About">
<div className="module-splash-screen">
<div className="module-splash-screen__logo module-img--150" />
<div className="version">{version}</div> return (
<div className="environment">{environment}</div> <TitleBarContainer
<div> platform={platform}
<a href="https://signal.org">signal.org</a> theme={theme}
</div> executeMenuRole={executeMenuRole}
<br /> title={i18n('aboutSignalDesktop')}
<div> >
<a <div className="About">
className="acknowledgments" <div className="module-splash-screen">
href="https://github.com/signalapp/Signal-Desktop/blob/main/ACKNOWLEDGMENTS.md" <div className="module-splash-screen__logo module-img--150" />
>
{i18n('softwareAcknowledgments')} <div className="version">{version}</div>
</a> <div className="environment">{environment}</div>
</div> <div>
<div> <a href="https://signal.org">signal.org</a>
<a className="privacy" href="https://signal.org/legal"> </div>
{i18n('privacyPolicy')} <br />
</a> <div>
<a
className="acknowledgments"
href="https://github.com/signalapp/Signal-Desktop/blob/main/ACKNOWLEDGMENTS.md"
>
{i18n('softwareAcknowledgments')}
</a>
</div>
<div>
<a className="privacy" href="https://signal.org/legal">
{i18n('privacyPolicy')}
</a>
</div>
</div> </div>
</div> </div>
</div> </TitleBarContainer>
); );
}; };

View file

@ -1,4 +1,4 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
@ -11,11 +11,16 @@ import { Inbox } from './Inbox';
import { SmartInstallScreen } from '../state/smart/InstallScreen'; import { SmartInstallScreen } from '../state/smart/InstallScreen';
import { StandaloneRegistration } from './StandaloneRegistration'; import { StandaloneRegistration } from './StandaloneRegistration';
import { ThemeType } from '../types/Util'; import { ThemeType } from '../types/Util';
import type { LocaleMessagesType } from '../types/I18N';
import { usePageVisibility } from '../hooks/usePageVisibility'; import { usePageVisibility } from '../hooks/usePageVisibility';
import { useReducedMotion } from '../hooks/useReducedMotion'; import { useReducedMotion } from '../hooks/useReducedMotion';
import type { MenuOptionsType, MenuActionType } from '../types/menu';
import { TitleBarContainer } from './TitleBarContainer';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
type PropsType = { type PropsType = {
appView: AppViewType; appView: AppViewType;
localeMessages: LocaleMessagesType;
openInbox: () => void; openInbox: () => void;
registerSingleDevice: (number: string, code: string) => Promise<void>; registerSingleDevice: (number: string, code: string) => Promise<void>;
renderCallManager: () => JSX.Element; renderCallManager: () => JSX.Element;
@ -28,6 +33,14 @@ type PropsType = {
token: string token: string
) => Promise<void>; ) => Promise<void>;
theme: ThemeType; theme: ThemeType;
isMaximized: boolean;
isFullScreen: boolean;
menuOptions: MenuOptionsType;
platform: string;
executeMenuRole: ExecuteMenuRoleType;
executeMenuAction: (action: MenuActionType) => void;
titleBarDoubleClick: () => void;
} & ComponentProps<typeof Inbox>; } & ComponentProps<typeof Inbox>;
export const App = ({ export const App = ({
@ -39,6 +52,11 @@ export const App = ({
i18n, i18n,
isCustomizingPreferredReactions, isCustomizingPreferredReactions,
isShowingStoriesView, isShowingStoriesView,
isMaximized,
isFullScreen,
menuOptions,
platform,
localeMessages,
renderCallManager, renderCallManager,
renderCustomizingPreferredReactionsModal, renderCustomizingPreferredReactionsModal,
renderGlobalModalContainer, renderGlobalModalContainer,
@ -49,6 +67,9 @@ export const App = ({
registerSingleDevice, registerSingleDevice,
theme, theme,
verifyConversationsStoppingSend, verifyConversationsStoppingSend,
executeMenuAction,
executeMenuRole,
titleBarDoubleClick,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
let contents; let contents;
@ -113,17 +134,31 @@ export const App = ({
}, [prefersReducedMotion]); }, [prefersReducedMotion]);
return ( return (
<div <TitleBarContainer
className={classNames({ title="Signal"
App: true, theme={theme}
'light-theme': theme === ThemeType.light, isMaximized={isMaximized}
'dark-theme': theme === ThemeType.dark, isFullScreen={isFullScreen}
})} platform={platform}
hasMenu
localeMessages={localeMessages}
menuOptions={menuOptions}
executeMenuRole={executeMenuRole}
executeMenuAction={executeMenuAction}
titleBarDoubleClick={titleBarDoubleClick}
> >
{renderGlobalModalContainer()} <div
{renderCallManager()} className={classNames({
{isShowingStoriesView && renderStories()} App: true,
{contents} 'light-theme': theme === ThemeType.light,
</div> 'dark-theme': theme === ThemeType.dark,
})}
>
{renderGlobalModalContainer()}
{renderCallManager()}
{isShowingStoriesView && renderStories()}
{contents}
</div>
</TitleBarContainer>
); );
}; };

View file

@ -25,6 +25,8 @@ const createProps = (): PropsType => ({
await sleep(5000); await sleep(5000);
return 'https://picsum.photos/1800/900'; return 'https://picsum.photos/1800/900';
}, },
executeMenuRole: action('executeMenuRole'),
platform: 'win32',
}); });
export default { export default {

View file

@ -10,10 +10,13 @@ import type { LocalizerType } from '../types/Util';
import { Spinner } from './Spinner'; import { Spinner } from './Spinner';
import { ToastDebugLogError } from './ToastDebugLogError'; import { ToastDebugLogError } from './ToastDebugLogError';
import { ToastLinkCopied } from './ToastLinkCopied'; import { ToastLinkCopied } from './ToastLinkCopied';
import { TitleBarContainer } from './TitleBarContainer';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
import { ToastLoadingFullLogs } from './ToastLoadingFullLogs'; import { ToastLoadingFullLogs } from './ToastLoadingFullLogs';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { createSupportUrl } from '../util/createSupportUrl'; import { createSupportUrl } from '../util/createSupportUrl';
import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useTheme } from '../hooks/useTheme';
enum LoadState { enum LoadState {
NotStarted, NotStarted,
@ -28,6 +31,8 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
fetchLogs: () => Promise<string>; fetchLogs: () => Promise<string>;
uploadLogs: (logs: string) => Promise<string>; uploadLogs: (logs: string) => Promise<string>;
platform: string;
executeMenuRole: ExecuteMenuRoleType;
}; };
enum ToastType { enum ToastType {
@ -42,6 +47,8 @@ export const DebugLogWindow = ({
i18n, i18n,
fetchLogs, fetchLogs,
uploadLogs, uploadLogs,
platform,
executeMenuRole,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [loadState, setLoadState] = useState<LoadState>(LoadState.NotStarted); const [loadState, setLoadState] = useState<LoadState>(LoadState.NotStarted);
const [logText, setLogText] = useState<string | undefined>(); const [logText, setLogText] = useState<string | undefined>();
@ -49,6 +56,8 @@ export const DebugLogWindow = ({
const [textAreaValue, setTextAreaValue] = useState<string>(i18n('loading')); const [textAreaValue, setTextAreaValue] = useState<string>(i18n('loading'));
const [toastType, setToastType] = useState<ToastType | undefined>(); const [toastType, setToastType] = useState<ToastType | undefined>();
const theme = useTheme();
useEscapeHandling(closeWindow); useEscapeHandling(closeWindow);
useEffect(() => { useEffect(() => {
@ -135,32 +144,41 @@ export const DebugLogWindow = ({
}); });
return ( return (
<div className="DebugLogWindow"> <TitleBarContainer
<div> platform={platform}
<div className="DebugLogWindow__title">{i18n('debugLogSuccess')}</div> theme={theme}
<p className="DebugLogWindow__subtitle"> executeMenuRole={executeMenuRole}
{i18n('debugLogSuccessNextSteps')} title={i18n('debugLog')}
</p> >
<div className="DebugLogWindow">
<div>
<div className="DebugLogWindow__title">
{i18n('debugLogSuccess')}
</div>
<p className="DebugLogWindow__subtitle">
{i18n('debugLogSuccessNextSteps')}
</p>
</div>
<div className="DebugLogWindow__container">
<input
className="DebugLogWindow__link"
readOnly
type="text"
value={publicLogURL}
/>
</div>
<div className="DebugLogWindow__footer">
<Button
onClick={() => openLinkInWebBrowser(supportURL)}
variant={ButtonVariant.Secondary}
>
{i18n('reportIssue')}
</Button>
<Button onClick={copyLog}>{i18n('debugLogCopy')}</Button>
</div>
{toastElement}
</div> </div>
<div className="DebugLogWindow__container"> </TitleBarContainer>
<input
className="DebugLogWindow__link"
readOnly
type="text"
value={publicLogURL}
/>
</div>
<div className="DebugLogWindow__footer">
<Button
onClick={() => openLinkInWebBrowser(supportURL)}
variant={ButtonVariant.Secondary}
>
{i18n('reportIssue')}
</Button>
<Button onClick={copyLog}>{i18n('debugLogCopy')}</Button>
</div>
{toastElement}
</div>
); );
} }
@ -170,43 +188,50 @@ export const DebugLogWindow = ({
loadState === LoadState.Started || loadState === LoadState.Submitting; loadState === LoadState.Started || loadState === LoadState.Submitting;
return ( return (
<div className="DebugLogWindow"> <TitleBarContainer
<div> platform={platform}
<div className="DebugLogWindow__title">{i18n('submitDebugLog')}</div> theme={theme}
<p className="DebugLogWindow__subtitle"> executeMenuRole={executeMenuRole}
{i18n('debugLogExplanation')} title={i18n('debugLog')}
</p> >
<div className="DebugLogWindow">
<div>
<div className="DebugLogWindow__title">{i18n('submitDebugLog')}</div>
<p className="DebugLogWindow__subtitle">
{i18n('debugLogExplanation')}
</p>
</div>
<div className="DebugLogWindow__container">
{isLoading ? (
<Spinner svgSize="normal" />
) : (
<textarea
className="DebugLogWindow__textarea"
readOnly
rows={5}
spellCheck={false}
value={textAreaValue}
/>
)}
</div>
<div className="DebugLogWindow__footer">
<Button
disabled={!canSave}
onClick={() => {
if (logText) {
downloadLog(logText);
}
}}
variant={ButtonVariant.Secondary}
>
{i18n('debugLogSave')}
</Button>
<Button disabled={!canSubmit} onClick={handleSubmit}>
{i18n('submit')}
</Button>
</div>
{toastElement}
</div> </div>
<div className="DebugLogWindow__container"> </TitleBarContainer>
{isLoading ? (
<Spinner svgSize="normal" />
) : (
<textarea
className="DebugLogWindow__textarea"
readOnly
rows={5}
spellCheck={false}
value={textAreaValue}
/>
)}
</div>
<div className="DebugLogWindow__footer">
<Button
disabled={!canSave}
onClick={() => {
if (logText) {
downloadLog(logText);
}
}}
variant={ButtonVariant.Secondary}
>
{i18n('debugLogSave')}
</Button>
<Button disabled={!canSubmit} onClick={handleSubmit}>
{i18n('submit')}
</Button>
</div>
{toastElement}
</div>
); );
}; };

View file

@ -103,8 +103,19 @@ export const ModalHost = React.memo(
useFocusTrap ? ( useFocusTrap ? (
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
// This is alright because the overlay covers the entire screen allowOutsideClick: ({ target }) => {
allowOutsideClick: false, if (!target || !(target instanceof HTMLElement)) {
return false;
}
const titleBar = document.querySelector(
'.TitleBarContainer__title'
);
if (titleBar?.contains(target)) {
return true;
}
return false;
},
}} }}
> >
{modalContent} {modalContent}

View file

@ -156,6 +156,9 @@ const createProps = (): PropsType => ({
onZoomFactorChange: action('onZoomFactorChange'), onZoomFactorChange: action('onZoomFactorChange'),
i18n, i18n,
executeMenuRole: action('executeMenuRole'),
platform: 'win32',
}); });
export default { export default {

View file

@ -29,6 +29,8 @@ import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode'; import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
import { Select } from './Select'; import { Select } from './Select';
import { Spinner } from './Spinner'; import { Spinner } from './Spinner';
import { TitleBarContainer } from './TitleBarContainer';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
import { getCustomColorStyle } from '../util/getCustomColorStyle'; import { getCustomColorStyle } from '../util/getCustomColorStyle';
import { import {
DEFAULT_DURATIONS_IN_SECONDS, DEFAULT_DURATIONS_IN_SECONDS,
@ -37,6 +39,7 @@ import {
} from '../util/expirationTimer'; } from '../util/expirationTimer';
import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useUniqueId } from '../hooks/useUniqueId'; import { useUniqueId } from '../hooks/useUniqueId';
import { useTheme } from '../hooks/useTheme';
type CheckboxChangeHandlerType = (value: boolean) => unknown; type CheckboxChangeHandlerType = (value: boolean) => unknown;
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown; type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
@ -99,6 +102,8 @@ export type PropsType = {
value: CustomColorType; value: CustomColorType;
} }
) => unknown; ) => unknown;
platform: string;
executeMenuRole: ExecuteMenuRoleType;
// Limited support features // Limited support features
isAudioNotificationsSupported: boolean; isAudioNotificationsSupported: boolean;
@ -193,6 +198,7 @@ export const Preferences = ({
doDeleteAllData, doDeleteAllData,
doneRendering, doneRendering,
editCustomColor, editCustomColor,
executeMenuRole,
getConversationsWithCustomColor, getConversationsWithCustomColor,
hasAudioNotifications, hasAudioNotifications,
hasAutoDownloadUpdate, hasAutoDownloadUpdate,
@ -250,6 +256,7 @@ export const Preferences = ({
onThemeChange, onThemeChange,
onUniversalExpireTimerChange, onUniversalExpireTimerChange,
onZoomFactorChange, onZoomFactorChange,
platform,
removeCustomColor, removeCustomColor,
removeCustomColorOnConversations, removeCustomColorOnConversations,
resetAllChatColors, resetAllChatColors,
@ -273,6 +280,7 @@ export const Preferences = ({
const [nowSyncing, setNowSyncing] = useState(false); const [nowSyncing, setNowSyncing] = useState(false);
const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] = const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] =
useState(false); useState(false);
const theme = useTheme();
useEffect(() => { useEffect(() => {
doneRendering(); doneRendering();
@ -1017,78 +1025,85 @@ export const Preferences = ({
} }
return ( return (
<div className="Preferences"> <TitleBarContainer
<div className="Preferences__page-selector"> platform={platform}
<button theme={theme}
type="button" executeMenuRole={executeMenuRole}
className={classNames({ title={i18n('signalDesktopPreferences')}
Preferences__button: true, >
'Preferences__button--general': true, <div className="Preferences">
'Preferences__button--selected': page === Page.General, <div className="Preferences__page-selector">
})} <button
onClick={() => setPage(Page.General)} type="button"
> className={classNames({
{i18n('Preferences__button--general')} Preferences__button: true,
</button> 'Preferences__button--general': true,
<button 'Preferences__button--selected': page === Page.General,
type="button" })}
className={classNames({ onClick={() => setPage(Page.General)}
Preferences__button: true, >
'Preferences__button--appearance': true, {i18n('Preferences__button--general')}
'Preferences__button--selected': </button>
page === Page.Appearance || page === Page.ChatColor, <button
})} type="button"
onClick={() => setPage(Page.Appearance)} className={classNames({
> Preferences__button: true,
{i18n('Preferences__button--appearance')} 'Preferences__button--appearance': true,
</button> 'Preferences__button--selected':
<button page === Page.Appearance || page === Page.ChatColor,
type="button" })}
className={classNames({ onClick={() => setPage(Page.Appearance)}
Preferences__button: true, >
'Preferences__button--chats': true, {i18n('Preferences__button--appearance')}
'Preferences__button--selected': page === Page.Chats, </button>
})} <button
onClick={() => setPage(Page.Chats)} type="button"
> className={classNames({
{i18n('Preferences__button--chats')} Preferences__button: true,
</button> 'Preferences__button--chats': true,
<button 'Preferences__button--selected': page === Page.Chats,
type="button" })}
className={classNames({ onClick={() => setPage(Page.Chats)}
Preferences__button: true, >
'Preferences__button--calls': true, {i18n('Preferences__button--chats')}
'Preferences__button--selected': page === Page.Calls, </button>
})} <button
onClick={() => setPage(Page.Calls)} type="button"
> className={classNames({
{i18n('Preferences__button--calls')} Preferences__button: true,
</button> 'Preferences__button--calls': true,
<button 'Preferences__button--selected': page === Page.Calls,
type="button" })}
className={classNames({ onClick={() => setPage(Page.Calls)}
Preferences__button: true, >
'Preferences__button--notifications': true, {i18n('Preferences__button--calls')}
'Preferences__button--selected': page === Page.Notifications, </button>
})} <button
onClick={() => setPage(Page.Notifications)} type="button"
> className={classNames({
{i18n('Preferences__button--notifications')} Preferences__button: true,
</button> 'Preferences__button--notifications': true,
<button 'Preferences__button--selected': page === Page.Notifications,
type="button" })}
className={classNames({ onClick={() => setPage(Page.Notifications)}
Preferences__button: true, >
'Preferences__button--privacy': true, {i18n('Preferences__button--notifications')}
'Preferences__button--selected': page === Page.Privacy, </button>
})} <button
onClick={() => setPage(Page.Privacy)} type="button"
> className={classNames({
{i18n('Preferences__button--privacy')} Preferences__button: true,
</button> 'Preferences__button--privacy': true,
'Preferences__button--selected': page === Page.Privacy,
})}
onClick={() => setPage(Page.Privacy)}
>
{i18n('Preferences__button--privacy')}
</button>
</div>
<div className="Preferences__settings-pane">{settings}</div>
</div> </div>
<div className="Preferences__settings-pane">{settings}</div> </TitleBarContainer>
</div>
); );
}; };

View file

@ -0,0 +1,185 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { ReactNode } from 'react';
import TitleBar from '@indutny/frameless-titlebar';
import type { MenuItem } from '@indutny/frameless-titlebar';
import type { MenuItemConstructorOptions } from 'electron';
import { createTemplate } from '../../app/menu';
import { ThemeType } from '../types/Util';
import type { LocaleMessagesType } from '../types/I18N';
import type { MenuOptionsType, MenuActionType } from '../types/menu';
export type MenuPropsType = Readonly<{
hasMenu: true;
localeMessages: LocaleMessagesType;
menuOptions: MenuOptionsType;
executeMenuAction: (action: MenuActionType) => void;
}>;
export type ExecuteMenuRoleType = (
role: MenuItemConstructorOptions['role']
) => void;
export type PropsType = Readonly<{
title: string;
theme: ThemeType;
isMaximized?: boolean;
isFullScreen?: boolean;
platform: string;
executeMenuRole: ExecuteMenuRoleType;
titleBarDoubleClick?: () => void;
children: ReactNode;
}> &
(MenuPropsType | { hasMenu?: false });
// Windows only
const ROLE_TO_ACCELERATOR = new Map<
MenuItemConstructorOptions['role'],
string
>();
ROLE_TO_ACCELERATOR.set('undo', 'CmdOrCtrl+Z');
ROLE_TO_ACCELERATOR.set('redo', 'CmdOrCtrl+Y');
ROLE_TO_ACCELERATOR.set('cut', 'CmdOrCtrl+X');
ROLE_TO_ACCELERATOR.set('copy', 'CmdOrCtrl+C');
ROLE_TO_ACCELERATOR.set('paste', 'CmdOrCtrl+V');
ROLE_TO_ACCELERATOR.set('pasteAndMatchStyle', 'CmdOrCtrl+Shift+V');
ROLE_TO_ACCELERATOR.set('selectAll', 'CmdOrCtrl+A');
ROLE_TO_ACCELERATOR.set('resetZoom', 'CmdOrCtrl+0');
ROLE_TO_ACCELERATOR.set('zoomIn', 'CmdOrCtrl+=');
ROLE_TO_ACCELERATOR.set('zoomOut', 'CmdOrCtrl+-');
ROLE_TO_ACCELERATOR.set('togglefullscreen', 'F11');
ROLE_TO_ACCELERATOR.set('toggleDevTools', 'CmdOrCtrl+Shift+I');
ROLE_TO_ACCELERATOR.set('minimize', 'CmdOrCtrl+M');
function convertMenu(
menuList: ReadonlyArray<MenuItemConstructorOptions>,
executeMenuRole: (role: MenuItemConstructorOptions['role']) => void
): Array<MenuItem> {
return menuList.map(item => {
const {
type,
label,
accelerator: originalAccelerator,
click: originalClick,
submenu: originalSubmenu,
role,
} = item;
let submenu: Array<MenuItem> | undefined;
if (Array.isArray(originalSubmenu)) {
submenu = convertMenu(originalSubmenu, executeMenuRole);
} else if (originalSubmenu) {
throw new Error('Non-array submenu is not supported');
}
let click: (() => unknown) | undefined;
if (originalClick) {
if (role) {
throw new Error(`Menu item: ${label} has both click and role`);
}
// We don't use arguments in app/menu.ts
click = originalClick as () => unknown;
} else if (role) {
click = () => executeMenuRole(role);
}
let accelerator: string | undefined;
if (originalAccelerator) {
accelerator = originalAccelerator.toString();
} else if (role) {
accelerator = ROLE_TO_ACCELERATOR.get(role);
}
return {
type,
label,
accelerator,
click,
submenu,
};
});
}
export const TitleBarContainer = (props: PropsType): JSX.Element => {
const {
title,
theme,
isMaximized,
isFullScreen,
executeMenuRole,
titleBarDoubleClick,
children,
platform,
hasMenu,
} = props;
if (platform !== 'win32' || isFullScreen) {
return <>{children}</>;
}
let maybeMenu: Array<MenuItem> | undefined;
if (hasMenu) {
const { localeMessages, menuOptions, executeMenuAction } = props;
const menuTemplate = createTemplate(
{
...menuOptions,
// actions
forceUpdate: () => executeMenuAction('forceUpdate'),
openContactUs: () => executeMenuAction('openContactUs'),
openForums: () => executeMenuAction('openForums'),
openJoinTheBeta: () => executeMenuAction('openJoinTheBeta'),
openReleaseNotes: () => executeMenuAction('openReleaseNotes'),
openSupportPage: () => executeMenuAction('openSupportPage'),
setupAsNewDevice: () => executeMenuAction('setupAsNewDevice'),
setupAsStandalone: () => executeMenuAction('setupAsStandalone'),
showAbout: () => executeMenuAction('showAbout'),
showDebugLog: () => executeMenuAction('showDebugLog'),
showKeyboardShortcuts: () => executeMenuAction('showKeyboardShortcuts'),
showSettings: () => executeMenuAction('showSettings'),
showStickerCreator: () => executeMenuAction('showStickerCreator'),
showWindow: () => executeMenuAction('showWindow'),
},
localeMessages
);
maybeMenu = convertMenu(menuTemplate, executeMenuRole);
}
const titleBarTheme = {
bar: {
palette:
theme === ThemeType.light ? ('light' as const) : ('dark' as const),
},
// Hide overlay
menu: {
overlay: {
opacity: 0,
},
},
};
return (
<div className="TitleBarContainer">
<TitleBar
className="TitleBarContainer__title"
platform={platform}
title={title}
iconSrc="images/icon_32.png"
theme={titleBarTheme}
maximized={isMaximized}
menu={maybeMenu}
onDoubleClick={titleBarDoubleClick}
hideControls
/>
<div className="TitleBarContainer__content">{children}</div>
</div>
);
};

View file

@ -27,6 +27,7 @@ export type SystemThemeHolder = { systemTheme: SystemThemeType };
export type NativeThemeType = { export type NativeThemeType = {
getSystemTheme: () => SystemThemeType; getSystemTheme: () => SystemThemeType;
subscribe: (fn: Callback) => void; subscribe: (fn: Callback) => void;
unsubscribe: (fn: Callback) => void;
update: () => SystemThemeType; update: () => SystemThemeType;
}; };
@ -50,6 +51,14 @@ export function createNativeThemeListener(
subscribers.push(fn); subscribers.push(fn);
} }
function unsubscribe(fn: Callback): void {
const index = subscribers.indexOf(fn);
if (index !== -1) {
subscribers.splice(index, 1);
}
}
ipc.on( ipc.on(
'native-theme:changed', 'native-theme:changed',
(_event: unknown, change: NativeThemeState) => { (_event: unknown, change: NativeThemeState) => {
@ -67,6 +76,7 @@ export function createNativeThemeListener(
return { return {
getSystemTheme: () => systemTheme, getSystemTheme: () => systemTheme,
subscribe, subscribe,
unsubscribe,
update, update,
}; };
} }

58
ts/hooks/useTheme.ts Normal file
View file

@ -0,0 +1,58 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useState, useEffect } from 'react';
import { ThemeType } from '../types/Util';
// Note that this hook is used in non-main windows (e.g. "About" and
// "Debug Log" windows), and thus can't access redux state.
export const useTheme = (): ThemeType => {
const [theme, updateTheme] = useState(ThemeType.light);
// Storybook support
const { SignalContext } = window;
useEffect(() => {
const abortController = new AbortController();
const { signal } = abortController;
async function applyTheme() {
let newTheme = await SignalContext.Settings.themeSetting.getValue();
if (newTheme === 'system') {
newTheme = SignalContext.nativeThemeListener.getSystemTheme();
}
if (signal.aborted) {
return;
}
if (newTheme === 'dark') {
updateTheme(ThemeType.dark);
} else {
updateTheme(ThemeType.light);
}
}
async function loop() {
while (!signal.aborted) {
// eslint-disable-next-line no-await-in-loop
await applyTheme();
// eslint-disable-next-line no-await-in-loop
await SignalContext.Settings.waitForChange();
}
}
SignalContext.nativeThemeListener.subscribe(applyTheme);
loop();
return () => {
abortController.abort();
SignalContext.nativeThemeListener.unsubscribe(applyTheme);
};
}, [updateTheme, SignalContext.Settings, SignalContext.nativeThemeListener]);
return theme;
};

View file

@ -3,6 +3,7 @@
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import { ipcMain as ipc, session } from 'electron'; import { ipcMain as ipc, session } from 'electron';
import { EventEmitter } from 'events';
import { userConfig } from '../../app/user_config'; import { userConfig } from '../../app/user_config';
import { ephemeralConfig } from '../../app/ephemeral_config'; import { ephemeralConfig } from '../../app/ephemeral_config';
@ -25,7 +26,7 @@ type ResponseQueueEntry = Readonly<{
reject(error: Error): void; reject(error: Error): void;
}>; }>;
export class SettingsChannel { export class SettingsChannel extends EventEmitter {
private mainWindow?: BrowserWindow; private mainWindow?: BrowserWindow;
private readonly responseQueue = new Map<number, ResponseQueueEntry>(); private readonly responseQueue = new Map<number, ResponseQueueEntry>();
@ -229,7 +230,7 @@ export class SettingsChannel {
return; return;
} }
ipc.handle(`settings:set:${name}`, (_event, value) => { ipc.handle(`settings:set:${name}`, async (_event, value) => {
if (isEphemeral) { if (isEphemeral) {
const ephemeralName = EPHEMERAL_NAME_MAP.get(name); const ephemeralName = EPHEMERAL_NAME_MAP.get(name);
strictAssert( strictAssert(
@ -239,7 +240,9 @@ export class SettingsChannel {
ephemeralConfig.set(ephemeralName, value); ephemeralConfig.set(ephemeralName, value);
} }
return this.setSettingInMainWindow(name, value); await this.setSettingInMainWindow(name, value);
this.emit(`change:${name}`, value);
}); });
} }
} }

View file

@ -5,8 +5,10 @@ import { trigger } from '../../shims/events';
import type { NoopActionType } from './noop'; import type { NoopActionType } from './noop';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { LocaleMessagesType } from '../../types/I18N';
import { ThemeType } from '../../types/Util'; import { ThemeType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID'; import type { UUIDStringType } from '../../types/UUID';
import type { MenuOptionsType } from '../../types/menu';
// State // State
@ -21,7 +23,11 @@ export type UserStateType = {
platform: string; platform: string;
regionCode: string | undefined; regionCode: string | undefined;
i18n: LocalizerType; i18n: LocalizerType;
localeMessages: LocaleMessagesType;
interactionMode: 'mouse' | 'keyboard'; interactionMode: 'mouse' | 'keyboard';
isMainWindowMaximized: boolean;
isMainWindowFullScreen: boolean;
menuOptions: MenuOptionsType;
theme: ThemeType; theme: ThemeType;
version: string; version: string;
}; };
@ -38,6 +44,9 @@ type UserChangedActionType = {
regionCode?: string; regionCode?: string;
interactionMode?: 'mouse' | 'keyboard'; interactionMode?: 'mouse' | 'keyboard';
theme?: ThemeType; theme?: ThemeType;
isMainWindowMaximized?: boolean;
isMainWindowFullScreen?: boolean;
menuOptions?: MenuOptionsType;
}; };
}; };
@ -58,6 +67,9 @@ function userChanged(attributes: {
ourUuid?: UUIDStringType; ourUuid?: UUIDStringType;
regionCode?: string; regionCode?: string;
theme?: ThemeType; theme?: ThemeType;
isMainWindowMaximized?: boolean;
isMainWindowFullScreen?: boolean;
menuOptions?: MenuOptionsType;
}): UserChangedActionType { }): UserChangedActionType {
return { return {
type: 'USER_CHANGED', type: 'USER_CHANGED',
@ -88,6 +100,15 @@ export function getEmptyState(): UserStateType {
regionCode: 'missing', regionCode: 'missing',
platform: 'missing', platform: 'missing',
interactionMode: 'mouse', interactionMode: 'mouse',
isMainWindowMaximized: false,
isMainWindowFullScreen: false,
menuOptions: {
development: false,
devTools: false,
includeSetup: false,
isProduction: true,
platform: 'unknown',
},
theme: ThemeType.light, theme: ThemeType.light,
i18n: Object.assign( i18n: Object.assign(
() => { () => {
@ -99,6 +120,7 @@ export function getEmptyState(): UserStateType {
}, },
} }
), ),
localeMessages: {},
version: '0.0.0', version: '0.0.0',
}; };
} }

View file

@ -25,14 +25,20 @@ import type { StateType } from './reducer';
import type { BadgesStateType } from './ducks/badges'; import type { BadgesStateType } from './ducks/badges';
import type { StoryDataType } from './ducks/stories'; import type { StoryDataType } from './ducks/stories';
import { getInitialState as stickers } from '../types/Stickers'; import { getInitialState as stickers } from '../types/Stickers';
import type { MenuOptionsType } from '../types/menu';
import { getEmojiReducerState as emojis } from '../util/loadRecentEmojis'; import { getEmojiReducerState as emojis } from '../util/loadRecentEmojis';
import type { MainWindowStatsType } from '../windows/context';
export function getInitialState({ export function getInitialState({
badges, badges,
stories, stories,
mainWindowStats,
menuOptions,
}: { }: {
badges: BadgesStateType; badges: BadgesStateType;
stories: Array<StoryDataType>; stories: Array<StoryDataType>;
mainWindowStats: MainWindowStatsType;
menuOptions: MenuOptionsType;
}): StateType { }): StateType {
const items = window.storage.getItemsState(); const items = window.storage.getItemsState();
@ -108,9 +114,13 @@ export function getInitialState({
ourUuid, ourUuid,
platform: window.platform, platform: window.platform,
i18n: window.i18n, i18n: window.i18n,
localeMessages: window.SignalContext.localeMessages,
interactionMode: window.getInteractionMode(), interactionMode: window.getInteractionMode(),
theme, theme,
version: window.getVersion(), version: window.getVersion(),
isMainWindowMaximized: mainWindowStats.isMaximized,
isMainWindowFullScreen: mainWindowStats.isFullScreen,
menuOptions,
}, },
}; };
} }

View file

@ -5,6 +5,8 @@ import { createSelector } from 'reselect';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID'; import type { UUIDStringType } from '../../types/UUID';
import type { LocaleMessagesType } from '../../types/I18N';
import type { MenuOptionsType } from '../../types/menu';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { UserStateType } from '../ducks/user'; import type { UserStateType } from '../ducks/user';
@ -43,6 +45,11 @@ export const getIntl = createSelector(
(state: UserStateType): LocalizerType => state.i18n (state: UserStateType): LocalizerType => state.i18n
); );
export const getLocaleMessages = createSelector(
getUser,
(state: UserStateType): LocaleMessagesType => state.localeMessages
);
export const getInteractionMode = createSelector( export const getInteractionMode = createSelector(
getUser, getUser,
(state: UserStateType) => state.interactionMode (state: UserStateType) => state.interactionMode
@ -81,3 +88,18 @@ const getVersion = createSelector(
export const getIsAlpha = createSelector(getVersion, isAlpha); export const getIsAlpha = createSelector(getVersion, isAlpha);
export const getIsBeta = createSelector(getVersion, isBeta); export const getIsBeta = createSelector(getVersion, isBeta);
export const getIsMainWindowMaximized = createSelector(
getUser,
(state: UserStateType): boolean => state.isMainWindowMaximized
);
export const getIsMainWindowFullScreen = createSelector(
getUser,
(state: UserStateType): boolean => state.isMainWindowFullScreen
);
export const getMenuOptions = createSelector(
getUser,
(state: UserStateType): MenuOptionsType => state.menuOptions
);

View file

@ -3,7 +3,9 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import type { MenuItemConstructorOptions } from 'electron';
import type { MenuActionType } from '../../types/menu';
import { App } from '../../components/App'; import { App } from '../../components/App';
import { SmartCallManager } from './CallManager'; import { SmartCallManager } from './CallManager';
import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal'; import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal';
@ -12,7 +14,15 @@ import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { SmartStories } from './Stories'; import { SmartStories } from './Stories';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user'; import {
getIntl,
getLocaleMessages,
getTheme,
getIsMainWindowMaximized,
getIsMainWindowFullScreen,
getMenuOptions,
getPlatform,
} from '../selectors/user';
import { shouldShowStoriesView } from '../selectors/stories'; import { shouldShowStoriesView } from '../selectors/stories';
import { getConversationsStoppingSend } from '../selectors/conversations'; import { getConversationsStoppingSend } from '../selectors/conversations';
import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions'; import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions';
@ -25,7 +35,12 @@ const mapStateToProps = (state: StateType) => {
conversationsStoppingSend: getConversationsStoppingSend(state), conversationsStoppingSend: getConversationsStoppingSend(state),
getPreferredBadge: getPreferredBadgeSelector(state), getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state), i18n: getIntl(state),
localeMessages: getLocaleMessages(state),
isCustomizingPreferredReactions: getIsCustomizingPreferredReactions(state), isCustomizingPreferredReactions: getIsCustomizingPreferredReactions(state),
isMaximized: getIsMainWindowMaximized(state),
isFullScreen: getIsMainWindowFullScreen(state),
menuOptions: getMenuOptions(state),
platform: getPlatform(state),
renderCallManager: () => <SmartCallManager />, renderCallManager: () => <SmartCallManager />,
renderCustomizingPreferredReactionsModal: () => ( renderCustomizingPreferredReactionsModal: () => (
<SmartCustomizingPreferredReactionsModal /> <SmartCustomizingPreferredReactionsModal />
@ -53,6 +68,16 @@ const mapStateToProps = (state: StateType) => {
return window.getAccountManager().registerSingleDevice(number, code); return window.getAccountManager().registerSingleDevice(number, code);
}, },
theme: getTheme(state), theme: getTheme(state),
executeMenuRole: (role: MenuItemConstructorOptions['role']): void => {
window.SignalContext.executeMenuRole(role);
},
executeMenuAction: (action: MenuActionType): void => {
window.SignalContext.executeMenuAction(action);
},
titleBarDoubleClick: (): void => {
window.titleBarDoubleClick();
},
}; };
}; };

View file

@ -5,9 +5,10 @@ import { assert } from 'chai';
import { stub } from 'sinon'; import { stub } from 'sinon';
import type { MenuItemConstructorOptions } from 'electron'; import type { MenuItemConstructorOptions } from 'electron';
import type { MenuListType, MenuOptionsType } from '../../../app/menu'; import type { CreateTemplateOptionsType } from '../../../app/menu';
import { createTemplate } from '../../../app/menu'; import { createTemplate } from '../../../app/menu';
import { load as loadLocale } from '../../../app/locale'; import { load as loadLocale } from '../../../app/locale';
import type { MenuListType } from '../../types/menu';
const forceUpdate = stub(); const forceUpdate = stub();
const openContactUs = stub(); const openContactUs = stub();
@ -53,13 +54,11 @@ const getExpectedEditMenu = (
], ],
}); });
const getExpectedViewMenu = ( const getExpectedViewMenu = (): MenuItemConstructorOptions => ({
zoomModifier: 'Command' | 'Control'
): MenuItemConstructorOptions => ({
label: '&View', label: '&View',
submenu: [ submenu: [
{ label: 'Actual Size', role: 'resetZoom' }, { label: 'Actual Size', role: 'resetZoom' },
{ accelerator: `${zoomModifier}+=`, label: 'Zoom In', role: 'zoomIn' }, { accelerator: 'CmdOrCtrl+=', label: 'Zoom In', role: 'zoomIn' },
{ label: 'Zoom Out', role: 'zoomOut' }, { label: 'Zoom Out', role: 'zoomOut' },
{ type: 'separator' }, { type: 'separator' },
{ label: 'Toggle Full Screen', role: 'togglefullscreen' }, { label: 'Toggle Full Screen', role: 'togglefullscreen' },
@ -127,7 +126,7 @@ const EXPECTED_MACOS: MenuListType = [
], ],
}, },
getExpectedEditMenu(true), getExpectedEditMenu(true),
getExpectedViewMenu('Command'), getExpectedViewMenu(),
{ {
label: '&Window', label: '&Window',
role: 'window', role: 'window',
@ -157,7 +156,7 @@ const EXPECTED_WINDOWS: MenuListType = [
], ],
}, },
getExpectedEditMenu(false), getExpectedEditMenu(false),
getExpectedViewMenu('Control'), getExpectedViewMenu(),
{ {
label: '&Window', label: '&Window',
role: 'window', role: 'window',
@ -226,7 +225,7 @@ describe('createTemplate', () => {
PLATFORMS.forEach(({ label, platform, expectedDefault }) => { PLATFORMS.forEach(({ label, platform, expectedDefault }) => {
describe(label, () => { describe(label, () => {
it('should return the correct template without setup options', () => { it('should return the correct template without setup options', () => {
const options: MenuOptionsType = { const options: CreateTemplateOptionsType = {
development: false, development: false,
devTools: true, devTools: true,
includeSetup: false, includeSetup: false,
@ -240,7 +239,7 @@ describe('createTemplate', () => {
}); });
it('should return correct template with setup options', () => { it('should return correct template with setup options', () => {
const options: MenuOptionsType = { const options: CreateTemplateOptionsType = {
development: false, development: false,
devTools: true, devTools: true,
includeSetup: true, includeSetup: true,

View file

@ -45,15 +45,6 @@ export const isHideMenuBarSupported = (): boolean => !OS.isMacOS();
// the "draw attention on notification" option is specific to Windows and Linux // the "draw attention on notification" option is specific to Windows and Linux
export const isDrawAttentionSupported = (): boolean => !OS.isMacOS(); export const isDrawAttentionSupported = (): boolean => !OS.isMacOS();
export enum TitleBarVisibility {
Visible,
Hidden,
}
// 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 * 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. * option with a command line flag, but that is not officially supported.

33
ts/types/menu.ts Normal file
View file

@ -0,0 +1,33 @@
// Copyright 2017-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MenuItemConstructorOptions } from 'electron';
export type MenuListType = Array<MenuItemConstructorOptions>;
export type MenuOptionsType = Readonly<{
development: boolean;
devTools: boolean;
includeSetup: boolean;
isProduction: boolean;
platform: string;
}>;
export type MenuActionsType = Readonly<{
forceUpdate: () => unknown;
openContactUs: () => unknown;
openForums: () => unknown;
openJoinTheBeta: () => unknown;
openReleaseNotes: () => unknown;
openSupportPage: () => unknown;
setupAsNewDevice: () => unknown;
setupAsStandalone: () => unknown;
showAbout: () => unknown;
showDebugLog: () => unknown;
showKeyboardShortcuts: () => unknown;
showSettings: () => unknown;
showStickerCreator: () => unknown;
showWindow: () => unknown;
}>;
export type MenuActionType = keyof MenuActionsType;

View file

@ -132,6 +132,160 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2021-04-05T20:48:36.065Z" "updated": "2021-04-05T20:48:36.065Z"
}, },
{
"rule": "React-createRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " .map(() => createRef());",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " const ref = useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " const ref = useRef();",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " const activeMenus = useRef((_a = menu === null || menu === void 0 ? void 0 : menu.length) !== null && _a !== void 0 ? _a : 0);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " const savedCallback = useRef(onClickAway);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " const menuRef = useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " const scrollRef = useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " myRef = myRef !== null && myRef !== void 0 ? myRef : useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " const overflowRef = useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " const menuBar = useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.es.js",
"line": " const ref = useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-createRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " .map(() => React.createRef());",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " const ref = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " const ref = React.useRef();",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " const activeMenus = React.useRef((_a = menu === null || menu === void 0 ? void 0 : menu.length) !== null && _a !== void 0 ? _a : 0);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " const savedCallback = React.useRef(onClickAway);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " const menuRef = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " const scrollRef = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " myRef = myRef !== null && myRef !== void 0 ? myRef : React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " const overflowRef = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " const menuBar = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@indutny/frameless-titlebar/dist/index.js",
"line": " const ref = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-06-06T22:58:37.359Z"
},
{ {
"rule": "jQuery-append(", "rule": "jQuery-append(",
"path": "node_modules/@jridgewell/source-map/dist/source-map.umd.js", "path": "node_modules/@jridgewell/source-map/dist/source-map.umd.js",

View file

@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { contextBridge, ipcRenderer } from 'electron'; import { contextBridge } from 'electron';
import { SignalContext } from '../context'; import { SignalContext } from '../context';
import { About } from '../../components/About'; import { About } from '../../components/About';
@ -29,10 +29,12 @@ contextBridge.exposeInMainWorld('SignalContext', {
ReactDOM.render( ReactDOM.render(
React.createElement(About, { React.createElement(About, {
closeAbout: () => ipcRenderer.send('close-about'), closeAbout: () => SignalContext.executeMenuRole('close'),
environment: `${environmentText.join(' - ')}${platform}`, environment: `${environmentText.join(' - ')}${platform}`,
i18n: SignalContext.i18n, i18n: SignalContext.i18n,
version: SignalContext.getVersion(), version: SignalContext.getVersion(),
platform: process.platform,
executeMenuRole: SignalContext.executeMenuRole,
}), }),
document.getElementById('app') document.getElementById('app')
); );

View file

@ -2,11 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import type { MenuItemConstructorOptions } from 'electron';
import url from 'url'; import url from 'url';
import type { ParsedUrlQuery } from 'querystring'; import type { ParsedUrlQuery } from 'querystring';
import type { MenuOptionsType, MenuActionType } from '../types/menu';
import type { IPCEventsValuesType } from '../util/createIPCEvents'; import type { IPCEventsValuesType } from '../util/createIPCEvents';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { LoggerType } from '../types/Logging'; import type { LoggerType } from '../types/Logging';
import type { LocaleMessagesType } from '../types/I18N';
import type { NativeThemeType } from '../context/createNativeThemeListener'; import type { NativeThemeType } from '../context/createNativeThemeListener';
import type { SettingType } from '../util/preload'; import type { SettingType } from '../util/preload';
import { Bytes } from '../context/Bytes'; import { Bytes } from '../context/Bytes';
@ -37,6 +40,11 @@ strictAssert(Boolean(window.SignalContext), 'context must be defined');
initializeLogging(); initializeLogging();
export type MainWindowStatsType = Readonly<{
isMaximized: boolean;
isFullScreen: boolean;
}>;
export type SignalContextType = { export type SignalContextType = {
bytes: Bytes; bytes: Bytes;
crypto: Crypto; crypto: Crypto;
@ -55,8 +63,13 @@ export type SignalContextType = {
getVersion: () => string; getVersion: () => string;
getPath: (name: 'userData' | 'home') => string; getPath: (name: 'userData' | 'home') => string;
i18n: LocalizerType; i18n: LocalizerType;
localeMessages: LocaleMessagesType;
log: LoggerType; log: LoggerType;
renderWindow?: () => void; renderWindow?: () => void;
executeMenuRole: (role: MenuItemConstructorOptions['role']) => Promise<void>;
getMainWindowStats: () => Promise<MainWindowStatsType>;
getMenuOptions: () => Promise<MenuOptionsType>;
executeMenuAction: (action: MenuActionType) => Promise<void>;
}; };
export const SignalContext: SignalContextType = { export const SignalContext: SignalContextType = {
@ -76,12 +89,27 @@ export const SignalContext: SignalContextType = {
return String(config[`${name}Path`]); return String(config[`${name}Path`]);
}, },
i18n: setupI18n(locale, localeMessages), i18n: setupI18n(locale, localeMessages),
localeMessages,
log: window.SignalContext.log, log: window.SignalContext.log,
nativeThemeListener: createNativeThemeListener(ipcRenderer, window), nativeThemeListener: createNativeThemeListener(ipcRenderer, window),
setIsCallActive(isCallActive: boolean): void { setIsCallActive(isCallActive: boolean): void {
ipcRenderer.send('set-is-call-active', isCallActive); ipcRenderer.send('set-is-call-active', isCallActive);
}, },
timers: new Timers(), timers: new Timers(),
async executeMenuRole(
role: MenuItemConstructorOptions['role']
): Promise<void> {
await ipcRenderer.invoke('executeMenuRole', role);
},
async getMainWindowStats(): Promise<MainWindowStatsType> {
return ipcRenderer.invoke('getMainWindowStats');
},
async getMenuOptions(): Promise<MenuOptionsType> {
return ipcRenderer.invoke('getMenuOptions');
},
async executeMenuAction(action: MenuActionType): Promise<void> {
return ipcRenderer.invoke('executeMenuAction', action);
},
}; };
window.SignalContext = SignalContext; window.SignalContext = SignalContext;

View file

@ -23,7 +23,9 @@ contextBridge.exposeInMainWorld('SignalContext', {
ReactDOM.render( ReactDOM.render(
React.createElement(DebugLogWindow, { React.createElement(DebugLogWindow, {
closeWindow: () => ipcRenderer.send('close-debug-log'), platform: process.platform,
executeMenuRole: SignalContext.executeMenuRole,
closeWindow: () => SignalContext.executeMenuRole('close'),
downloadLog: (logText: string) => downloadLog: (logText: string) =>
ipcRenderer.send('show-debug-log-save-dialog', logText), ipcRenderer.send('show-debug-log-save-dialog', logText),
i18n: SignalContext.i18n, i18n: SignalContext.i18n,

View file

@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { contextBridge, ipcRenderer } from 'electron'; import { contextBridge } from 'electron';
import { SignalContext } from '../context'; import { SignalContext } from '../context';
@ -40,7 +40,7 @@ contextBridge.exposeInMainWorld('SignalContext', {
} }
function onClose() { function onClose() {
ipcRenderer.send('close-permissions-popup'); SignalContext.executeMenuRole('close');
} }
ReactDOM.render( ReactDOM.render(

View file

@ -13,9 +13,10 @@ contextBridge.exposeInMainWorld('SignalContext', SignalContext);
function renderScreenSharingController(presentedSourceName: string): void { function renderScreenSharingController(presentedSourceName: string): void {
ReactDOM.render( ReactDOM.render(
React.createElement(CallingScreenSharingController, { React.createElement(CallingScreenSharingController, {
platform: process.platform,
executeMenuRole: SignalContext.executeMenuRole,
i18n: SignalContext.i18n, i18n: SignalContext.i18n,
onCloseController: () => onCloseController: () => SignalContext.executeMenuRole('close'),
ipcRenderer.send('close-screen-share-controller'),
onStopSharing: () => ipcRenderer.send('stop-screen-share'), onStopSharing: () => ipcRenderer.send('stop-screen-share'),
presentedSourceName, presentedSourceName,
}), }),

View file

@ -247,7 +247,7 @@ const renderPreferences = async () => {
// Actions and other props // Actions and other props
addCustomColor: ipcAddCustomColor, addCustomColor: ipcAddCustomColor,
closeSettings: () => ipcRenderer.send('close-settings'), closeSettings: () => SignalContext.executeMenuRole('close'),
doDeleteAllData: () => ipcRenderer.send('delete-all-data'), doDeleteAllData: () => ipcRenderer.send('delete-all-data'),
doneRendering, doneRendering,
editCustomColor: ipcEditCustomColor, editCustomColor: ipcEditCustomColor,
@ -337,6 +337,9 @@ const renderPreferences = async () => {
onZoomFactorChange: settingZoomFactor.setValue, onZoomFactorChange: settingZoomFactor.setValue,
i18n: SignalContext.i18n, i18n: SignalContext.i18n,
platform: process.platform,
executeMenuRole: SignalContext.executeMenuRole,
}; };
function reRender<Value>(f: (value: Value) => Promise<Value>) { function reRender<Value>(f: (value: Value) => Promise<Value>) {

View file

@ -1378,6 +1378,14 @@
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
"@indutny/frameless-titlebar@2.1.4-rc.8":
version "2.1.4-rc.8"
resolved "https://registry.yarnpkg.com/@indutny/frameless-titlebar/-/frameless-titlebar-2.1.4-rc.8.tgz#e315d9c0199e769f8d7811d67d9b821658b66afb"
integrity sha512-R9gXCfe4LA6K0urEiCKT3h9hxcg4Z/BZZxbMp607sQ7z2bfLaCAVCm7WdAkl2piOPMNorYCWd7Vo2oG6krGmVg==
dependencies:
classnames "^2.2.6"
deepmerge "^4.2.2"
"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": "@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3":
version "0.1.3" version "0.1.3"
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
@ -5545,10 +5553,10 @@ classnames@2.2.5:
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
integrity sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0= integrity sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=
classnames@^2.2.5: classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6" version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-css@^4.2.3: clean-css@^4.2.3:
version "4.2.3" version "4.2.3"