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

View File

@ -5,6 +5,30 @@
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
License: MIT

View File

@ -20,6 +20,11 @@
rel="stylesheet"
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" />
</head>
<body>

View File

@ -27,6 +27,10 @@ import {
shell,
systemPreferences,
} from 'electron';
import type {
MenuItemConstructorOptions,
TitleBarOverlayOptions,
} from 'electron';
import { z } from 'zod';
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 * as sqlChannels from './sql_channel';
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 { installFileHandler, installWebHandler } from './protocol_filter';
import * as OS from '../ts/OS';
@ -91,10 +96,6 @@ import {
} from '../ts/util/sgnlHref';
import { clearTimeoutIfNecessary } from '../ts/util/clearTimeoutIfNecessary';
import { toggleMaximizedBrowserWindow } from '../ts/util/toggleMaximizedBrowserWindow';
import {
getTitleBarVisibility,
TitleBarVisibility,
} from '../ts/types/Settings';
import { ChallengeMainHandler } from '../ts/main/challengeMain';
import { NativeThemeNotifier } from '../ts/main/NativeThemeNotifier';
import { PowerChannel } from '../ts/main/powerChannel';
@ -324,6 +325,8 @@ if (windowFromUserConfig) {
ephemeralConfig.set('window', windowConfig);
}
let menuOptions: CreateTemplateOptionsType | undefined;
// These will be set after app fires the 'ready' event
let logger: LoggerType | 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('new-window', handleUrl);
window.webContents.on(
@ -467,6 +473,23 @@ function handleCommonWindowEvents(window: BrowserWindow) {
window.webContents.on('preferred-size-changed', onZoomChanged);
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;
@ -521,10 +544,50 @@ if (OS.isWindows()) {
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() {
const usePreloadBundle =
!isTestEnvironment(getEnvironment()) || forcePreloadBundle;
const titleBarOverlay = await getTitleBarOverlay();
const windowOptions: Electron.BrowserWindowConstructorOptions = {
show: false,
width: DEFAULT_WIDTH,
@ -532,11 +595,8 @@ async function createWindow() {
minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT,
autoHideMenuBar: false,
titleBarStyle:
getTitleBarVisibility() === TitleBarVisibility.Hidden &&
!isTestEnvironment(getEnvironment())
? 'hidden'
: 'default',
titleBarStyle: mainTitleBarStyle,
titleBarOverlay,
backgroundColor: isTestEnvironment(getEnvironment())
? '#ffffff' // Tests should always be rendered on a white background
: await getBackgroundColor(),
@ -616,7 +676,20 @@ async function createWindow() {
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) {
return;
}
@ -624,8 +697,7 @@ async function createWindow() {
const size = mainWindow.getSize();
const position = mainWindow.getPosition();
// so if we need to recreate the window, we have the most recent settings
windowConfig = {
const newWindowConfig = {
maximized: mainWindow.isMaximized(),
autoHideMenuBar: mainWindow.autoHideMenuBar,
fullscreen: mainWindow.isFullScreen(),
@ -635,16 +707,24 @@ async function createWindow() {
y: position[1],
};
getLogger().info(
'Updating BrowserWindow config: %s',
JSON.stringify(windowConfig)
);
ephemeralConfig.set('window', windowConfig);
if (
newWindowConfig.fullscreen !== windowConfig?.fullscreen ||
newWindowConfig.maximized !== windowConfig?.maximized
) {
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', debouncedCaptureStats);
mainWindow.on('move', debouncedCaptureStats);
mainWindow.on('resize', captureWindowStats);
mainWindow.on('move', captureWindowStats);
const setWindowFocus = () => {
if (!mainWindow) {
@ -681,7 +761,7 @@ async function createWindow() {
mainWindow.webContents.openDevTools();
}
handleCommonWindowEvents(mainWindow);
handleCommonWindowEvents(mainWindow, titleBarOverlay);
// App dock icon bounce
bounce.init(mainWindow);
@ -981,6 +1061,7 @@ function showScreenShareWindow(sourceName: string) {
resizable: false,
show: false,
title: getLocale().i18n('screenShareWindow'),
titleBarStyle: nonMainTitleBarStyle,
width,
webPreferences: {
...defaultWebPrefs,
@ -1021,11 +1102,15 @@ async function showAbout() {
return;
}
const titleBarOverlay = await getTitleBarOverlay();
const options = {
width: 500,
height: 500,
resizable: false,
title: getLocale().i18n('aboutSignalDesktop'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(),
show: false,
@ -1041,7 +1126,7 @@ async function showAbout() {
aboutWindow = new BrowserWindow(options);
handleCommonWindowEvents(aboutWindow);
handleCommonWindowEvents(aboutWindow, titleBarOverlay);
aboutWindow.loadURL(prepareFileUrl([__dirname, '../about.html']));
@ -1063,12 +1148,16 @@ async function showSettingsWindow() {
return;
}
const titleBarOverlay = await getTitleBarOverlay();
const options = {
width: 700,
height: 700,
frame: true,
resizable: false,
title: getLocale().i18n('signalDesktopPreferences'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(),
show: false,
@ -1084,7 +1173,7 @@ async function showSettingsWindow() {
settingsWindow = new BrowserWindow(options);
handleCommonWindowEvents(settingsWindow);
handleCommonWindowEvents(settingsWindow, titleBarOverlay);
settingsWindow.loadURL(prepareFileUrl([__dirname, '../settings.html']));
@ -1132,6 +1221,7 @@ async function showStickerCreator() {
const { x = 0, y = 0 } = windowConfig || {};
// TODO: DESKTOP-3670
const options = {
x: x + 100,
y: y + 100,
@ -1191,12 +1281,16 @@ async function showDebugLogWindow() {
return;
}
const titleBarOverlay = await getTitleBarOverlay();
const theme = await getThemeSetting();
const options = {
width: 700,
height: 500,
resizable: false,
title: getLocale().i18n('debugLog'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(),
show: false,
@ -1218,7 +1312,7 @@ async function showDebugLogWindow() {
debugLogWindow = new BrowserWindow(options);
handleCommonWindowEvents(debugLogWindow);
handleCommonWindowEvents(debugLogWindow, titleBarOverlay);
debugLogWindow.loadURL(
prepareFileUrl([__dirname, '../debug_log.html'], { theme })
@ -1259,6 +1353,7 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) {
height: Math.min(150, size[1]),
resizable: false,
title: getLocale().i18n('allowAccess'),
titleBarStyle: nonMainTitleBarStyle,
autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(),
show: false,
@ -1681,9 +1776,9 @@ app.on('ready', async () => {
]);
});
function setupMenu(options?: Partial<MenuOptionsType>) {
function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
const { platform } = process;
const menuOptions = {
menuOptions = {
// options
development,
devTools: defaultWebPrefs.devTools,
@ -1713,6 +1808,14 @@ function setupMenu(options?: Partial<MenuOptionsType>) {
const template = createTemplate(menuOptions, getLocale().messages);
const menu = Menu.buildFromTemplate(template);
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() {
@ -1910,12 +2013,6 @@ ipc.on(
}
);
ipc.on('close-about', () => {
if (aboutWindow) {
aboutWindow.close();
}
});
ipc.on('close-screen-share-controller', () => {
if (screenShareWindow) {
screenShareWindow.close();
@ -1941,11 +2038,6 @@ ipc.on('update-tray-icon', (_event: Electron.Event, unreadCount: number) => {
// Debug Log-related IPC calls
ipc.on('show-debug-log', showDebugLogWindow);
ipc.on('close-debug-log', () => {
if (debugLogWindow) {
debugLogWindow.close();
}
});
ipc.on(
'show-debug-log-save-dialog',
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
@ -1993,11 +2080,6 @@ function removeDarkOverlay() {
}
ipc.on('show-settings', showSettingsWindow);
ipc.on('close-settings', () => {
if (settingsWindow) {
settingsWindow.close();
}
});
ipc.on('delete-all-data', () => {
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())) {
ipc.handle('ci:test-electron:done', async (_event, info) => {
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
import { isString } from 'lodash';
import type { MenuItemConstructorOptions } from 'electron';
import type { LocaleMessagesType } from '../ts/types/I18N';
import type {
MenuListType,
MenuOptionsType,
MenuActionsType,
} from '../ts/types/menu';
export type MenuListType = Array<MenuItemConstructorOptions>;
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 type CreateTemplateOptionsType = MenuOptionsType & MenuActionsType;
export const createTemplate = (
options: MenuOptionsType,
options: CreateTemplateOptionsType,
messages: LocaleMessagesType
): MenuListType => {
if (!isString(options.platform)) {
@ -131,7 +110,7 @@ export const createTemplate = (
label: messages.viewMenuResetZoom.message,
},
{
accelerator: platform === 'darwin' ? 'Command+=' : 'Control+=',
accelerator: 'CmdOrCtrl+=',
role: 'zoomIn',
label: messages.viewMenuZoomIn.message,
},
@ -265,7 +244,7 @@ export const createTemplate = (
function updateForMac(
template: MenuListType,
messages: LocaleMessagesType,
options: MenuOptionsType
options: CreateTemplateOptionsType
): MenuListType {
const { showAbout, showSettings, showWindow } = options;

View File

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

View File

@ -23,7 +23,7 @@
img-src 'self' blob: data:;
media-src 'self' blob:;
object-src 'none';
script-src 'self' 'sha256-eLeGwSfPmXJ+EUiLfIeXABvLiUqDbiKgNLpHITaabgQ=';
script-src 'self' 'sha256-Qu05oqDmBO5fZacm7tr/oerJcqsW0G/XqP4PRCziovc=' 'sha256-eLeGwSfPmXJ+EUiLfIeXABvLiUqDbiKgNLpHITaabgQ=';
style-src 'self' 'unsafe-inline';"
/>
<title>Signal</title>
@ -81,6 +81,11 @@
rel="stylesheet"
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" />
<!--
@ -152,6 +157,12 @@
</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="ts/set_os_class.js"></script>
<script
@ -164,13 +175,6 @@
src="ts/backbone/reliable_trigger.js"
></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
type="text/javascript"
src="js/views/react_wrapper_view.js"

View File

@ -20,6 +20,11 @@
rel="stylesheet"
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" />
</head>
<body>

View File

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

View File

@ -233,6 +233,20 @@ try {
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 =>
ipc.send('challenge:request', request);

View File

@ -20,6 +20,11 @@
rel="stylesheet"
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" />
</head>
<body>

View File

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

View File

@ -4,6 +4,11 @@
<!DOCTYPE html>
<html>
<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" />
</head>
<body>

View File

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

View File

@ -27,6 +27,14 @@ body {
--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 {
background-color: $color-white;
color: $color-gray-90;
@ -236,6 +244,9 @@ $loading-height: 16px;
top: 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
* theme class.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,10 +4,10 @@
.StoryViewer {
&__overlay {
background-size: contain;
height: 100vh;
height: var(--window-height);
left: 0;
position: absolute;
top: 0;
top: var(--titlebar-height);
width: 100%;
z-index: $z-index-popup-overlay;
}
@ -18,11 +18,11 @@
background: $color-black-alpha-20;
display: flex;
flex-direction: column;
height: 100vh;
height: var(--window-height);
justify-content: center;
left: 0;
position: absolute;
top: 0;
top: var(--titlebar-height);
width: 100%;
z-index: $z-index-popup-overlay;
}
@ -238,7 +238,7 @@
}
&__animated-emojis {
height: 100vh;
height: var(--window-height);
position: absolute;
width: 100%;
z-index: $z-index-above-base;
@ -248,7 +248,7 @@
@include button-reset;
align-items: center;
display: flex;
height: 100vh;
height: var(--window-height);
position: absolute;
width: 25%;
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/TimelineWarning.scss';
@import './components/TimelineWarnings.scss';
@import './components/TitleBarContainer.scss';
@import './components/Toast.scss';
@import './components/WhatsNew.scss';

View File

@ -26,8 +26,8 @@ import * as Bytes from './Bytes';
import * as Timers from './Timers';
import * as indexedDb from './indexeddb';
import type { WhatIsThis } from './window.d';
import type { MenuOptionsType } from './types/menu';
import type { Receipt } from './types/Receipt';
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
import { SocketStatus } from './types/SocketStatus';
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
import { ThemeType } from './types/Util';
@ -141,6 +141,7 @@ import { ToastConversationArchived } from './components/ToastConversationArchive
import { ToastConversationUnarchived } from './components/ToastConversationUnarchived';
import { showToast } from './util/showToast';
import { startInteractionMode } from './windows/startInteractionMode';
import type { MainWindowStatsType } from './windows/context';
import { deliveryReceiptsJobQueue } from './jobs/deliveryReceiptsJobQueue';
import { updateOurUsername } from './util/updateOurUsername';
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) => {
const target = event.target as HTMLElement;
if (isWindowDragElement(target)) {
@ -930,6 +931,19 @@ export async function startApp(): Promise<void> {
}
}, FIVE_MINUTES);
let mainWindowStats = {
isMaximized: false,
isFullScreen: false,
};
let menuOptions = {
development: false,
devTools: false,
includeSetup: false,
isProduction: true,
platform: 'unknown',
};
try {
await Promise.all([
window.ConversationController.load(),
@ -938,6 +952,12 @@ export async function startApp(): Promise<void> {
loadInitialBadgesState(),
loadStories(),
window.textsecure.storage.protocol.hydrateCaches(),
(async () => {
mainWindowStats = await window.SignalContext.getMainWindowStats();
})(),
(async () => {
menuOptions = await window.SignalContext.getMenuOptions();
})(),
]);
await window.ConversationController.checkForConflicts();
} catch (error) {
@ -946,7 +966,7 @@ export async function startApp(): Promise<void> {
error && error.stack ? error.stack : error
);
} finally {
initializeRedux();
initializeRedux({ mainWindowStats, menuOptions });
start();
window.Signal.Services.initializeNetworkObserver(
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
const convoCollection = window.getConversations();
const initialState = getInitialState({
badges: initialBadgesState,
stories: getStoriesForRedux(),
mainWindowStats,
menuOptions,
});
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;
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
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useTheme } from '../hooks/useTheme';
import { TitleBarContainer } from './TitleBarContainer';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
export type PropsType = {
closeAbout: () => unknown;
environment: string;
i18n: LocalizerType;
version: string;
platform: string;
executeMenuRole: ExecuteMenuRoleType;
};
export const About = ({
@ -17,34 +23,45 @@ export const About = ({
i18n,
environment,
version,
platform,
executeMenuRole,
}: PropsType): JSX.Element => {
useEscapeHandling(closeAbout);
return (
<div className="About">
<div className="module-splash-screen">
<div className="module-splash-screen__logo module-img--150" />
const theme = useTheme();
<div className="version">{version}</div>
<div className="environment">{environment}</div>
<div>
<a href="https://signal.org">signal.org</a>
</div>
<br />
<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>
return (
<TitleBarContainer
platform={platform}
theme={theme}
executeMenuRole={executeMenuRole}
title={i18n('aboutSignalDesktop')}
>
<div className="About">
<div className="module-splash-screen">
<div className="module-splash-screen__logo module-img--150" />
<div className="version">{version}</div>
<div className="environment">{environment}</div>
<div>
<a href="https://signal.org">signal.org</a>
</div>
<br />
<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>
</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
import type { ComponentProps } from 'react';
@ -11,11 +11,16 @@ import { Inbox } from './Inbox';
import { SmartInstallScreen } from '../state/smart/InstallScreen';
import { StandaloneRegistration } from './StandaloneRegistration';
import { ThemeType } from '../types/Util';
import type { LocaleMessagesType } from '../types/I18N';
import { usePageVisibility } from '../hooks/usePageVisibility';
import { useReducedMotion } from '../hooks/useReducedMotion';
import type { MenuOptionsType, MenuActionType } from '../types/menu';
import { TitleBarContainer } from './TitleBarContainer';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
type PropsType = {
appView: AppViewType;
localeMessages: LocaleMessagesType;
openInbox: () => void;
registerSingleDevice: (number: string, code: string) => Promise<void>;
renderCallManager: () => JSX.Element;
@ -28,6 +33,14 @@ type PropsType = {
token: string
) => Promise<void>;
theme: ThemeType;
isMaximized: boolean;
isFullScreen: boolean;
menuOptions: MenuOptionsType;
platform: string;
executeMenuRole: ExecuteMenuRoleType;
executeMenuAction: (action: MenuActionType) => void;
titleBarDoubleClick: () => void;
} & ComponentProps<typeof Inbox>;
export const App = ({
@ -39,6 +52,11 @@ export const App = ({
i18n,
isCustomizingPreferredReactions,
isShowingStoriesView,
isMaximized,
isFullScreen,
menuOptions,
platform,
localeMessages,
renderCallManager,
renderCustomizingPreferredReactionsModal,
renderGlobalModalContainer,
@ -49,6 +67,9 @@ export const App = ({
registerSingleDevice,
theme,
verifyConversationsStoppingSend,
executeMenuAction,
executeMenuRole,
titleBarDoubleClick,
}: PropsType): JSX.Element => {
let contents;
@ -113,17 +134,31 @@ export const App = ({
}, [prefersReducedMotion]);
return (
<div
className={classNames({
App: true,
'light-theme': theme === ThemeType.light,
'dark-theme': theme === ThemeType.dark,
})}
<TitleBarContainer
title="Signal"
theme={theme}
isMaximized={isMaximized}
isFullScreen={isFullScreen}
platform={platform}
hasMenu
localeMessages={localeMessages}
menuOptions={menuOptions}
executeMenuRole={executeMenuRole}
executeMenuAction={executeMenuAction}
titleBarDoubleClick={titleBarDoubleClick}
>
{renderGlobalModalContainer()}
{renderCallManager()}
{isShowingStoriesView && renderStories()}
{contents}
</div>
<div
className={classNames({
App: true,
'light-theme': theme === ThemeType.light,
'dark-theme': theme === ThemeType.dark,
})}
>
{renderGlobalModalContainer()}
{renderCallManager()}
{isShowingStoriesView && renderStories()}
{contents}
</div>
</TitleBarContainer>
);
};

View File

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

View File

@ -10,10 +10,13 @@ import type { LocalizerType } from '../types/Util';
import { Spinner } from './Spinner';
import { ToastDebugLogError } from './ToastDebugLogError';
import { ToastLinkCopied } from './ToastLinkCopied';
import { TitleBarContainer } from './TitleBarContainer';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
import { ToastLoadingFullLogs } from './ToastLoadingFullLogs';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { createSupportUrl } from '../util/createSupportUrl';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useTheme } from '../hooks/useTheme';
enum LoadState {
NotStarted,
@ -28,6 +31,8 @@ export type PropsType = {
i18n: LocalizerType;
fetchLogs: () => Promise<string>;
uploadLogs: (logs: string) => Promise<string>;
platform: string;
executeMenuRole: ExecuteMenuRoleType;
};
enum ToastType {
@ -42,6 +47,8 @@ export const DebugLogWindow = ({
i18n,
fetchLogs,
uploadLogs,
platform,
executeMenuRole,
}: PropsType): JSX.Element => {
const [loadState, setLoadState] = useState<LoadState>(LoadState.NotStarted);
const [logText, setLogText] = useState<string | undefined>();
@ -49,6 +56,8 @@ export const DebugLogWindow = ({
const [textAreaValue, setTextAreaValue] = useState<string>(i18n('loading'));
const [toastType, setToastType] = useState<ToastType | undefined>();
const theme = useTheme();
useEscapeHandling(closeWindow);
useEffect(() => {
@ -135,32 +144,41 @@ export const DebugLogWindow = ({
});
return (
<div className="DebugLogWindow">
<div>
<div className="DebugLogWindow__title">{i18n('debugLogSuccess')}</div>
<p className="DebugLogWindow__subtitle">
{i18n('debugLogSuccessNextSteps')}
</p>
<TitleBarContainer
platform={platform}
theme={theme}
executeMenuRole={executeMenuRole}
title={i18n('debugLog')}
>
<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 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>
</TitleBarContainer>
);
}
@ -170,43 +188,50 @@ export const DebugLogWindow = ({
loadState === LoadState.Started || loadState === LoadState.Submitting;
return (
<div className="DebugLogWindow">
<div>
<div className="DebugLogWindow__title">{i18n('submitDebugLog')}</div>
<p className="DebugLogWindow__subtitle">
{i18n('debugLogExplanation')}
</p>
<TitleBarContainer
platform={platform}
theme={theme}
executeMenuRole={executeMenuRole}
title={i18n('debugLog')}
>
<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 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>
</TitleBarContainer>
);
};

View File

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

View File

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

View File

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

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 = {
getSystemTheme: () => SystemThemeType;
subscribe: (fn: Callback) => void;
unsubscribe: (fn: Callback) => void;
update: () => SystemThemeType;
};
@ -50,6 +51,14 @@ export function createNativeThemeListener(
subscribers.push(fn);
}
function unsubscribe(fn: Callback): void {
const index = subscribers.indexOf(fn);
if (index !== -1) {
subscribers.splice(index, 1);
}
}
ipc.on(
'native-theme:changed',
(_event: unknown, change: NativeThemeState) => {
@ -67,6 +76,7 @@ export function createNativeThemeListener(
return {
getSystemTheme: () => systemTheme,
subscribe,
unsubscribe,
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 { ipcMain as ipc, session } from 'electron';
import { EventEmitter } from 'events';
import { userConfig } from '../../app/user_config';
import { ephemeralConfig } from '../../app/ephemeral_config';
@ -25,7 +26,7 @@ type ResponseQueueEntry = Readonly<{
reject(error: Error): void;
}>;
export class SettingsChannel {
export class SettingsChannel extends EventEmitter {
private mainWindow?: BrowserWindow;
private readonly responseQueue = new Map<number, ResponseQueueEntry>();
@ -229,7 +230,7 @@ export class SettingsChannel {
return;
}
ipc.handle(`settings:set:${name}`, (_event, value) => {
ipc.handle(`settings:set:${name}`, async (_event, value) => {
if (isEphemeral) {
const ephemeralName = EPHEMERAL_NAME_MAP.get(name);
strictAssert(
@ -239,7 +240,9 @@ export class SettingsChannel {
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 { LocalizerType } from '../../types/Util';
import type { LocaleMessagesType } from '../../types/I18N';
import { ThemeType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID';
import type { MenuOptionsType } from '../../types/menu';
// State
@ -21,7 +23,11 @@ export type UserStateType = {
platform: string;
regionCode: string | undefined;
i18n: LocalizerType;
localeMessages: LocaleMessagesType;
interactionMode: 'mouse' | 'keyboard';
isMainWindowMaximized: boolean;
isMainWindowFullScreen: boolean;
menuOptions: MenuOptionsType;
theme: ThemeType;
version: string;
};
@ -38,6 +44,9 @@ type UserChangedActionType = {
regionCode?: string;
interactionMode?: 'mouse' | 'keyboard';
theme?: ThemeType;
isMainWindowMaximized?: boolean;
isMainWindowFullScreen?: boolean;
menuOptions?: MenuOptionsType;
};
};
@ -58,6 +67,9 @@ function userChanged(attributes: {
ourUuid?: UUIDStringType;
regionCode?: string;
theme?: ThemeType;
isMainWindowMaximized?: boolean;
isMainWindowFullScreen?: boolean;
menuOptions?: MenuOptionsType;
}): UserChangedActionType {
return {
type: 'USER_CHANGED',
@ -88,6 +100,15 @@ export function getEmptyState(): UserStateType {
regionCode: 'missing',
platform: 'missing',
interactionMode: 'mouse',
isMainWindowMaximized: false,
isMainWindowFullScreen: false,
menuOptions: {
development: false,
devTools: false,
includeSetup: false,
isProduction: true,
platform: 'unknown',
},
theme: ThemeType.light,
i18n: Object.assign(
() => {
@ -99,6 +120,7 @@ export function getEmptyState(): UserStateType {
},
}
),
localeMessages: {},
version: '0.0.0',
};
}

View File

@ -25,14 +25,20 @@ import type { StateType } from './reducer';
import type { BadgesStateType } from './ducks/badges';
import type { StoryDataType } from './ducks/stories';
import { getInitialState as stickers } from '../types/Stickers';
import type { MenuOptionsType } from '../types/menu';
import { getEmojiReducerState as emojis } from '../util/loadRecentEmojis';
import type { MainWindowStatsType } from '../windows/context';
export function getInitialState({
badges,
stories,
mainWindowStats,
menuOptions,
}: {
badges: BadgesStateType;
stories: Array<StoryDataType>;
mainWindowStats: MainWindowStatsType;
menuOptions: MenuOptionsType;
}): StateType {
const items = window.storage.getItemsState();
@ -108,9 +114,13 @@ export function getInitialState({
ourUuid,
platform: window.platform,
i18n: window.i18n,
localeMessages: window.SignalContext.localeMessages,
interactionMode: window.getInteractionMode(),
theme,
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 { UUIDStringType } from '../../types/UUID';
import type { LocaleMessagesType } from '../../types/I18N';
import type { MenuOptionsType } from '../../types/menu';
import type { StateType } from '../reducer';
import type { UserStateType } from '../ducks/user';
@ -43,6 +45,11 @@ export const getIntl = createSelector(
(state: UserStateType): LocalizerType => state.i18n
);
export const getLocaleMessages = createSelector(
getUser,
(state: UserStateType): LocaleMessagesType => state.localeMessages
);
export const getInteractionMode = createSelector(
getUser,
(state: UserStateType) => state.interactionMode
@ -81,3 +88,18 @@ const getVersion = createSelector(
export const getIsAlpha = createSelector(getVersion, isAlpha);
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 { connect } from 'react-redux';
import type { MenuItemConstructorOptions } from 'electron';
import type { MenuActionType } from '../../types/menu';
import { App } from '../../components/App';
import { SmartCallManager } from './CallManager';
import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal';
@ -12,7 +14,15 @@ import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { SmartStories } from './Stories';
import type { StateType } from '../reducer';
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 { getConversationsStoppingSend } from '../selectors/conversations';
import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions';
@ -25,7 +35,12 @@ const mapStateToProps = (state: StateType) => {
conversationsStoppingSend: getConversationsStoppingSend(state),
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
localeMessages: getLocaleMessages(state),
isCustomizingPreferredReactions: getIsCustomizingPreferredReactions(state),
isMaximized: getIsMainWindowMaximized(state),
isFullScreen: getIsMainWindowFullScreen(state),
menuOptions: getMenuOptions(state),
platform: getPlatform(state),
renderCallManager: () => <SmartCallManager />,
renderCustomizingPreferredReactionsModal: () => (
<SmartCustomizingPreferredReactionsModal />
@ -53,6 +68,16 @@ const mapStateToProps = (state: StateType) => {
return window.getAccountManager().registerSingleDevice(number, code);
},
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 type { MenuItemConstructorOptions } from 'electron';
import type { MenuListType, MenuOptionsType } from '../../../app/menu';
import type { CreateTemplateOptionsType } from '../../../app/menu';
import { createTemplate } from '../../../app/menu';
import { load as loadLocale } from '../../../app/locale';
import type { MenuListType } from '../../types/menu';
const forceUpdate = stub();
const openContactUs = stub();
@ -53,13 +54,11 @@ const getExpectedEditMenu = (
],
});
const getExpectedViewMenu = (
zoomModifier: 'Command' | 'Control'
): MenuItemConstructorOptions => ({
const getExpectedViewMenu = (): MenuItemConstructorOptions => ({
label: '&View',
submenu: [
{ label: 'Actual Size', role: 'resetZoom' },
{ accelerator: `${zoomModifier}+=`, label: 'Zoom In', role: 'zoomIn' },
{ accelerator: 'CmdOrCtrl+=', label: 'Zoom In', role: 'zoomIn' },
{ label: 'Zoom Out', role: 'zoomOut' },
{ type: 'separator' },
{ label: 'Toggle Full Screen', role: 'togglefullscreen' },
@ -127,7 +126,7 @@ const EXPECTED_MACOS: MenuListType = [
],
},
getExpectedEditMenu(true),
getExpectedViewMenu('Command'),
getExpectedViewMenu(),
{
label: '&Window',
role: 'window',
@ -157,7 +156,7 @@ const EXPECTED_WINDOWS: MenuListType = [
],
},
getExpectedEditMenu(false),
getExpectedViewMenu('Control'),
getExpectedViewMenu(),
{
label: '&Window',
role: 'window',
@ -226,7 +225,7 @@ describe('createTemplate', () => {
PLATFORMS.forEach(({ label, platform, expectedDefault }) => {
describe(label, () => {
it('should return the correct template without setup options', () => {
const options: MenuOptionsType = {
const options: CreateTemplateOptionsType = {
development: false,
devTools: true,
includeSetup: false,
@ -240,7 +239,7 @@ describe('createTemplate', () => {
});
it('should return correct template with setup options', () => {
const options: MenuOptionsType = {
const options: CreateTemplateOptionsType = {
development: false,
devTools: 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
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
* 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",
"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(",
"path": "node_modules/@jridgewell/source-map/dist/source-map.umd.js",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -247,7 +247,7 @@ const renderPreferences = async () => {
// Actions and other props
addCustomColor: ipcAddCustomColor,
closeSettings: () => ipcRenderer.send('close-settings'),
closeSettings: () => SignalContext.executeMenuRole('close'),
doDeleteAllData: () => ipcRenderer.send('delete-all-data'),
doneRendering,
editCustomColor: ipcEditCustomColor,
@ -337,6 +337,9 @@ const renderPreferences = async () => {
onZoomFactorChange: settingZoomFactor.setValue,
i18n: SignalContext.i18n,
platform: process.platform,
executeMenuRole: SignalContext.executeMenuRole,
};
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"
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":
version "0.1.3"
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"
integrity sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=
classnames@^2.2.5:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
classnames@^2.2.5, classnames@^2.2.6:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-css@^4.2.3:
version "4.2.3"