Use patched frameless-titlebar on Windows
This commit is contained in:
parent
79c52847cd
commit
5634601554
54 changed files with 1343 additions and 323 deletions
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
298
app/main.ts
298
app/main.ts
|
@ -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) {
|
||||
|
|
41
app/menu.ts
41
app/menu.ts
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
14
preload.js
14
preload.js
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
top: var(--titlebar-height);
|
||||
z-index: $z-index-popup;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
}
|
||||
|
||||
&__settings-pane {
|
||||
height: 100vh;
|
||||
height: var(--window-height);
|
||||
overflow: overlay;
|
||||
width: 100%;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
31
stylesheets/components/TitleBarContainer.scss
Normal file
31
stylesheets/components/TitleBarContainer.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,6 +25,8 @@ const createProps = (): PropsType => ({
|
|||
await sleep(5000);
|
||||
return 'https://picsum.photos/1800/900';
|
||||
},
|
||||
executeMenuRole: action('executeMenuRole'),
|
||||
platform: 'win32',
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -156,6 +156,9 @@ const createProps = (): PropsType => ({
|
|||
onZoomFactorChange: action('onZoomFactorChange'),
|
||||
|
||||
i18n,
|
||||
|
||||
executeMenuRole: action('executeMenuRole'),
|
||||
platform: 'win32',
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
185
ts/components/TitleBarContainer.tsx
Normal file
185
ts/components/TitleBarContainer.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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
58
ts/hooks/useTheme.ts
Normal 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;
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
33
ts/types/menu.ts
Normal 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;
|
|
@ -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",
|
||||
|
|
|
@ -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')
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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>) {
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue