signal-desktop/app/main.ts

2450 lines
67 KiB
TypeScript
Raw Normal View History

// Copyright 2017-2022 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2022-06-23 19:15:27 +00:00
// This has to be the first import because it patches "os" module
import '../ts/util/patchWindows7Hostname';
2021-10-01 18:49:59 +00:00
import { join, normalize } from 'path';
import { pathToFileURL } from 'url';
import * as os from 'os';
import { chmod, realpath, writeFile } from 'fs-extra';
2021-10-01 18:49:59 +00:00
import { randomBytes } from 'crypto';
import normalizePath from 'normalize-path';
import fastGlob from 'fast-glob';
import PQueue from 'p-queue';
import { get, pick, isNumber, isBoolean, some, debounce, noop } from 'lodash';
import {
app,
BrowserWindow,
2021-03-04 21:44:57 +00:00
clipboard,
desktopCapturer,
2019-12-17 20:25:57 +00:00
dialog,
2021-10-01 18:49:59 +00:00
ipcMain as ipc,
Menu,
2022-05-11 22:58:14 +00:00
nativeTheme,
2021-12-10 22:53:10 +00:00
powerSaveBlocker,
2021-10-01 18:49:59 +00:00
protocol as electronProtocol,
screen,
session,
shell,
systemPreferences,
2021-10-01 18:49:59 +00:00
} from 'electron';
import type {
MenuItemConstructorOptions,
TitleBarOverlayOptions,
} from 'electron';
2021-10-01 18:49:59 +00:00
import { z } from 'zod';
2021-10-01 18:49:59 +00:00
import packageJson from '../package.json';
import * as GlobalErrors from './global_errors';
2022-01-11 20:02:46 +00:00
import { setup as setupCrashReports } from './crashReports';
2021-10-01 18:49:59 +00:00
import { setup as setupSpellChecker } from './spell_check';
import { redactAll, addSensitivePath } from '../ts/util/privacy';
import { createSupportUrl } from '../ts/util/createSupportUrl';
2022-05-11 22:58:14 +00:00
import { missingCaseError } from '../ts/util/missingCaseError';
import { strictAssert } from '../ts/util/assert';
import { consoleLogger } from '../ts/util/consoleLogger';
import type { ThemeSettingType } from '../ts/types/StorageUIKeys';
2022-05-11 22:58:14 +00:00
import { ThemeType } from '../ts/types/Util';
2021-10-01 18:49:59 +00:00
import './startup_config';
import type { ConfigType } from './config';
import type { RendererConfigType } from '../ts/types/RendererConfig';
2022-06-15 01:15:33 +00:00
import {
directoryConfigSchema,
rendererConfigSchema,
} from '../ts/types/RendererConfig';
import config from './config';
2021-10-01 18:49:59 +00:00
import {
Environment,
getEnvironment,
isTestEnvironment,
} from '../ts/environment';
// Very important to put before the single instance check, since it is based on the
2021-10-01 18:49:59 +00:00
// userData directory. (see requestSingleInstanceLock below)
import * as userConfig from './user_config';
2021-03-26 02:02:53 +00:00
// We generally want to pull in our own modules after this point, after the user
// data directory has been set.
2021-10-01 18:49:59 +00:00
import * as attachments from './attachments';
import * as attachmentChannel from './attachment_channel';
import * as bounce from '../ts/services/bounce';
import * as updater from '../ts/updater/index';
import { updateDefaultSession } from './updateDefaultSession';
2021-12-10 22:53:10 +00:00
import { PreventDisplaySleepService } from './PreventDisplaySleepService';
2021-10-01 18:49:59 +00:00
import { SystemTrayService } from './SystemTrayService';
import { SystemTraySettingCache } from './SystemTraySettingCache';
import {
SystemTraySetting,
shouldMinimizeToSystemTray,
parseSystemTraySetting,
2021-10-01 18:49:59 +00:00
} from '../ts/types/SystemTraySetting';
import * as ephemeralConfig from './ephemeral_config';
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 { CreateTemplateOptionsType } from './menu';
import type { MenuActionType } from '../ts/types/menu';
import { createTemplate } from './menu';
2021-10-01 18:49:59 +00:00
import { installFileHandler, installWebHandler } from './protocol_filter';
import * as OS from '../ts/OS';
import { isProduction } from '../ts/util/version';
2021-10-01 18:49:59 +00:00
import {
isSgnlHref,
isCaptchaHref,
isSignalHttpsLink,
parseSgnlHref,
parseCaptchaHref,
parseSignalHttpsLink,
2022-02-02 18:29:01 +00:00
rewriteSignalHrefsIfNecessary,
2021-10-01 18:49:59 +00:00
} from '../ts/util/sgnlHref';
import { clearTimeoutIfNecessary } from '../ts/util/clearTimeoutIfNecessary';
2021-10-01 18:49:59 +00:00
import { toggleMaximizedBrowserWindow } from '../ts/util/toggleMaximizedBrowserWindow';
import { ChallengeMainHandler } from '../ts/main/challengeMain';
import { NativeThemeNotifier } from '../ts/main/NativeThemeNotifier';
import { PowerChannel } from '../ts/main/powerChannel';
import { SettingsChannel } from '../ts/main/settingsChannel';
import { maybeParseUrl, setUrlSearchParams } from '../ts/util/url';
import { getHeicConverter } from '../ts/workers/heicConverterMain';
import type { LocaleType } from './locale';
import { load as loadLocale } from './locale';
2021-10-01 18:49:59 +00:00
import type { LoggerType } from '../ts/types/Logging';
const animationSettings = systemPreferences.getAnimationSettings();
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow: BrowserWindow | undefined;
let mainWindowCreated = false;
let loadingWindow: BrowserWindow | undefined;
const activeWindows = new Set<BrowserWindow>();
function getMainWindow() {
return mainWindow;
}
const development =
getEnvironment() === Environment.Development ||
getEnvironment() === Environment.Staging;
const isThrottlingEnabled = development || !isProduction(app.getVersion());
2021-10-01 18:49:59 +00:00
const enableCI = config.get<boolean>('enableCI');
const forcePreloadBundle = config.get<boolean>('forcePreloadBundle');
Beta versions support: SxS support, in-app env/instance display (#1606) * Script for beta config; unique data dir, in-app env/type display To release a beta build, increment the version and add -beta-N to the end, then go through all the standard release activities. The prepare-build npm script then updates key bits of the package.json to ensure that the beta build can be installed alongside a production build. This includes a new name ('Signal Beta') and a different location for application data. Note: Beta builds can be installed alongside production builds. As part of this, a couple new bits of data are shown across the app: - Environment (development or test, not shown if production) - App Instance (disabled in production; used for multiple accounts) These are shown in: - The window title - both environment and app instance. You can tell beta builds because the app name, preceding these data bits, is different. - The about window - both environment and app instance. You can tell beta builds from the version number. - The header added to the debug log - just environment. The version number will tell us if it's a beta build, and app instance isn't helpful. * Turn on single-window mode in non-production modes Because it's really frightening when you see 'unable to read from db' errors in the console. * aply.sh: More instructions for initial setup and testing * Gruntfile: Get consistent with use of package.json datas * Linux: manually update desktop keys, since macros not available
2017-10-30 20:57:13 +00:00
2021-12-10 22:53:10 +00:00
const preventDisplaySleepService = new PreventDisplaySleepService(
powerSaveBlocker
);
const challengeHandler = new ChallengeMainHandler();
const nativeThemeNotifier = new NativeThemeNotifier();
nativeThemeNotifier.initialize();
2020-03-20 21:00:11 +00:00
let appStartInitialSpellcheckSetting = true;
const defaultWebPrefs = {
devTools:
process.argv.some(arg => arg === '--enable-dev-tools') ||
2021-10-01 18:49:59 +00:00
getEnvironment() !== Environment.Production ||
2021-08-06 21:21:01 +00:00
!isProduction(app.getVersion()),
spellcheck: false,
};
function showWindow() {
if (!mainWindow) {
return;
}
// Using focus() instead of show() seems to be important on Windows when our window
// has been docked using Aero Snap/Snap Assist. A full .show() call here will cause
// the window to reposition:
// https://github.com/signalapp/Signal-Desktop/issues/1429
if (mainWindow.isVisible()) {
mainWindow.focus();
} else {
mainWindow.show();
}
}
Beta versions support: SxS support, in-app env/instance display (#1606) * Script for beta config; unique data dir, in-app env/type display To release a beta build, increment the version and add -beta-N to the end, then go through all the standard release activities. The prepare-build npm script then updates key bits of the package.json to ensure that the beta build can be installed alongside a production build. This includes a new name ('Signal Beta') and a different location for application data. Note: Beta builds can be installed alongside production builds. As part of this, a couple new bits of data are shown across the app: - Environment (development or test, not shown if production) - App Instance (disabled in production; used for multiple accounts) These are shown in: - The window title - both environment and app instance. You can tell beta builds because the app name, preceding these data bits, is different. - The about window - both environment and app instance. You can tell beta builds from the version number. - The header added to the debug log - just environment. The version number will tell us if it's a beta build, and app instance isn't helpful. * Turn on single-window mode in non-production modes Because it's really frightening when you see 'unable to read from db' errors in the console. * aply.sh: More instructions for initial setup and testing * Gruntfile: Get consistent with use of package.json datas * Linux: manually update desktop keys, since macros not available
2017-10-30 20:57:13 +00:00
if (!process.mas) {
console.log('making app single instance');
2019-02-21 22:41:17 +00:00
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
Beta versions support: SxS support, in-app env/instance display (#1606) * Script for beta config; unique data dir, in-app env/type display To release a beta build, increment the version and add -beta-N to the end, then go through all the standard release activities. The prepare-build npm script then updates key bits of the package.json to ensure that the beta build can be installed alongside a production build. This includes a new name ('Signal Beta') and a different location for application data. Note: Beta builds can be installed alongside production builds. As part of this, a couple new bits of data are shown across the app: - Environment (development or test, not shown if production) - App Instance (disabled in production; used for multiple accounts) These are shown in: - The window title - both environment and app instance. You can tell beta builds because the app name, preceding these data bits, is different. - The about window - both environment and app instance. You can tell beta builds from the version number. - The header added to the debug log - just environment. The version number will tell us if it's a beta build, and app instance isn't helpful. * Turn on single-window mode in non-production modes Because it's really frightening when you see 'unable to read from db' errors in the console. * aply.sh: More instructions for initial setup and testing * Gruntfile: Get consistent with use of package.json datas * Linux: manually update desktop keys, since macros not available
2017-10-30 20:57:13 +00:00
console.log('quitting; we are the second instance');
app.exit();
2019-02-21 22:41:17 +00:00
} else {
2021-10-01 18:49:59 +00:00
app.on('second-instance', (_e: Electron.Event, argv: Array<string>) => {
2019-02-21 22:41:17 +00:00
// Someone tried to run a second instance, we should focus our window
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
showWindow();
}
if (!logger) {
console.log(
'second-instance: logger not initialized; skipping further checks'
);
return;
}
const incomingCaptchaHref = getIncomingCaptchaHref(argv);
if (incomingCaptchaHref) {
2021-10-01 18:49:59 +00:00
const { captcha } = parseCaptchaHref(incomingCaptchaHref, getLogger());
challengeHandler.handleCaptcha(captcha);
return true;
}
2020-08-27 19:44:51 +00:00
// Are they trying to open a sgnl:// href?
const incomingHref = getIncomingHref(argv);
if (incomingHref) {
handleSgnlHref(incomingHref);
}
// Handled
2019-02-21 22:41:17 +00:00
return true;
});
2017-04-25 00:26:05 +00:00
}
}
2021-10-01 18:49:59 +00:00
/* eslint-enable no-console */
2017-04-25 00:26:05 +00:00
let sqlInitTimeStart = 0;
let sqlInitTimeEnd = 0;
const sql = new MainSQL();
const heicConverter = getHeicConverter();
async function getSpellCheckSetting() {
const fastValue = ephemeralConfig.get('spell-check');
if (fastValue !== undefined) {
getLogger().info('got fast spellcheck setting', fastValue);
return fastValue;
}
const json = await sql.sqlCall('getItemById', ['spell-check']);
// Default to `true` if setting doesn't exist yet
const slowValue = json ? json.value : true;
ephemeralConfig.set('spell-check', slowValue);
getLogger().info('got slow spellcheck setting', slowValue);
return slowValue;
}
2022-05-11 22:58:14 +00:00
type GetThemeSettingOptionsType = Readonly<{
ephemeralOnly?: boolean;
}>;
async function getThemeSetting({
ephemeralOnly = false,
}: GetThemeSettingOptionsType = {}): Promise<ThemeSettingType> {
const fastValue = ephemeralConfig.get('theme-setting');
if (fastValue !== undefined) {
getLogger().info('got fast theme-setting value', fastValue);
return fastValue as ThemeSettingType;
}
if (ephemeralOnly) {
return 'system';
}
const json = await sql.sqlCall('getItemById', ['theme-setting']);
// Default to `system` if setting doesn't exist or is invalid
const setting: unknown = json?.value;
const slowValue =
setting === 'light' || setting === 'dark' || setting === 'system'
? setting
: 'system';
2022-05-11 22:58:14 +00:00
ephemeralConfig.set('theme-setting', slowValue);
getLogger().info('got slow theme-setting value', slowValue);
return slowValue;
}
async function getResolvedThemeSetting(
options?: GetThemeSettingOptionsType
): Promise<ThemeType> {
const theme = await getThemeSetting(options);
if (theme === 'system') {
return nativeTheme.shouldUseDarkColors ? ThemeType.dark : ThemeType.light;
}
return ThemeType[theme];
}
async function getBackgroundColor(
options?: GetThemeSettingOptionsType
): Promise<string> {
const theme = await getResolvedThemeSetting(options);
if (theme === 'light') {
return '#3a76f0';
}
if (theme === 'dark') {
return '#121212';
}
throw missingCaseError(theme);
}
let systemTrayService: SystemTrayService | undefined;
const systemTraySettingCache = new SystemTraySettingCache(
sql,
ephemeralConfig,
process.argv,
app.getVersion()
);
const windowFromUserConfig = userConfig.get('window');
const windowFromEphemeral = ephemeralConfig.get('window');
2021-10-01 18:49:59 +00:00
export const windowConfigSchema = z.object({
maximized: z.boolean().optional(),
autoHideMenuBar: z.boolean().optional(),
fullscreen: z.boolean().optional(),
width: z.number(),
height: z.number(),
x: z.number(),
y: z.number(),
});
type WindowConfigType = z.infer<typeof windowConfigSchema>;
let windowConfig: WindowConfigType | undefined;
const windowConfigParsed = windowConfigSchema.safeParse(
windowFromEphemeral || windowFromUserConfig
);
if (windowConfigParsed.success) {
windowConfig = windowConfigParsed.data;
}
if (windowFromUserConfig) {
userConfig.set('window', null);
ephemeralConfig.set('window', windowConfig);
}
let menuOptions: CreateTemplateOptionsType | undefined;
2021-10-01 18:49:59 +00:00
// These will be set after app fires the 'ready' event
let logger: LoggerType | undefined;
let locale: LocaleType | undefined;
let settingsChannel: SettingsChannel | undefined;
2021-10-01 18:49:59 +00:00
function getLogger(): LoggerType {
if (!logger) {
console.warn('getLogger: Logger not yet initialized!');
return consoleLogger;
2021-10-01 18:49:59 +00:00
}
return logger;
}
function getLocale(): LocaleType {
if (!locale) {
throw new Error('getLocale: Locale not yet initialized!');
}
return locale;
}
type PrepareUrlOptions = { forCalling?: boolean; forCamera?: boolean };
async function prepareFileUrl(
2021-10-01 18:49:59 +00:00
pathSegments: ReadonlyArray<string>,
options: PrepareUrlOptions = {}
): Promise<string> {
2021-10-01 18:49:59 +00:00
const filePath = join(...pathSegments);
const fileUrl = pathToFileURL(filePath);
return prepareUrl(fileUrl, options);
}
async function prepareUrl(
2021-10-01 18:49:59 +00:00
url: URL,
{ forCalling, forCamera }: PrepareUrlOptions = {}
): Promise<string> {
const theme = await getResolvedThemeSetting();
2022-06-15 01:15:33 +00:00
const directoryConfig = directoryConfigSchema.safeParse({
2022-03-09 19:28:40 +00:00
directoryVersion: config.get<number | undefined>('directoryVersion') || 1,
directoryUrl: config.get<string | null>('directoryUrl') || undefined,
directoryEnclaveId:
config.get<string | null>('directoryEnclaveId') || undefined,
directoryTrustAnchor:
config.get<string | null>('directoryTrustAnchor') || undefined,
directoryV2Url: config.get<string | null>('directoryV2Url') || undefined,
directoryV2PublicKey:
config.get<string | null>('directoryV2PublicKey') || undefined,
directoryV2CodeHashes:
config.get<Array<string> | null>('directoryV2CodeHashes') || undefined,
2022-06-15 01:15:33 +00:00
directoryV3Url: config.get<string | null>('directoryV3Url') || undefined,
directoryV3MRENCLAVE:
config.get<string | null>('directoryV3MRENCLAVE') || undefined,
directoryV3Root: config.get<string | null>('directoryV3Root') || undefined,
});
if (!directoryConfig.success) {
throw new Error(
`prepareUrl: Failed to parse renderer directory config ${JSON.stringify(
directoryConfig.error.flatten()
)}`
);
}
const urlParams: RendererConfigType = {
name: packageJson.productName,
locale: getLocale().name,
version: app.getVersion(),
buildCreation: config.get<number>('buildCreation'),
buildExpiration: config.get<number>('buildExpiration'),
serverUrl: config.get<string>('serverUrl'),
storageUrl: config.get<string>('storageUrl'),
updatesUrl: config.get<string>('updatesUrl'),
2021-10-01 18:49:59 +00:00
cdnUrl0: config.get<ConfigType>('cdn').get<string>('0'),
cdnUrl2: config.get<ConfigType>('cdn').get<string>('2'),
certificateAuthority: config.get<string>('certificateAuthority'),
environment: enableCI ? Environment.Production : getEnvironment(),
enableCI,
nodeVersion: process.versions.node,
2022-06-23 19:15:27 +00:00
hostname: os.hostname(),
appInstance: process.env.NODE_APP_INSTANCE || undefined,
proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy || undefined,
2021-10-01 18:49:59 +00:00
contentProxyUrl: config.get<string>('contentProxyUrl'),
sfuUrl: config.get('sfuUrl'),
reducedMotionSetting: animationSettings.prefersReducedMotion,
2021-10-01 18:49:59 +00:00
serverPublicParams: config.get<string>('serverPublicParams'),
serverTrustRoot: config.get<string>('serverTrustRoot'),
theme,
appStartInitialSpellcheckSetting,
2021-10-27 17:54:16 +00:00
userDataPath: app.getPath('userData'),
homePath: app.getPath('home'),
2022-01-20 01:50:16 +00:00
crashDumpsPath: app.getPath('crashDumps'),
2022-06-15 01:15:33 +00:00
directoryConfig: directoryConfig.data,
// Only used by the main window
isMainWindowFullScreen: Boolean(mainWindow?.isFullScreen()),
2022-07-05 16:44:53 +00:00
isMainWindowMaximized: Boolean(mainWindow?.isMaximized()),
// Only for tests
argv: JSON.stringify(process.argv),
// Only for permission popup window
forCalling: Boolean(forCalling),
forCamera: Boolean(forCamera),
};
const parsed = rendererConfigSchema.safeParse(urlParams);
if (!parsed.success) {
throw new Error(
`prepareUrl: Failed to parse renderer config ${JSON.stringify(
parsed.error.flatten()
)}`
);
}
return setUrlSearchParams(url, { config: JSON.stringify(parsed.data) }).href;
}
2022-02-02 18:29:01 +00:00
async function handleUrl(event: Electron.Event, rawTarget: string) {
event.preventDefault();
2022-02-02 18:29:01 +00:00
const parsedUrl = maybeParseUrl(rawTarget);
if (!parsedUrl) {
return;
}
2022-02-02 18:29:01 +00:00
const target = rewriteSignalHrefsIfNecessary(rawTarget);
const { protocol, hostname } = parsedUrl;
2021-10-01 18:49:59 +00:00
const isDevServer =
process.env.SIGNAL_ENABLE_HTTP && hostname === 'localhost';
2019-12-17 20:25:57 +00:00
// We only want to specially handle urls that aren't requesting the dev server
2021-10-01 18:49:59 +00:00
if (
isSgnlHref(target, getLogger()) ||
isSignalHttpsLink(target, getLogger())
) {
handleSgnlHref(target);
return;
}
2019-12-17 20:25:57 +00:00
if ((protocol === 'http:' || protocol === 'https:') && !isDevServer) {
try {
await shell.openExternal(target);
} catch (error) {
2021-10-01 18:49:59 +00:00
getLogger().error(`Failed to open url: ${error.stack}`);
}
}
}
function handleCommonWindowEvents(
window: BrowserWindow,
titleBarOverlay: TitleBarOverlayOptions | false = false
) {
window.webContents.on('will-navigate', handleUrl);
window.webContents.on('new-window', handleUrl);
2021-10-01 18:49:59 +00:00
window.webContents.on(
'preload-error',
(_event: Electron.Event, preloadPath: string, error: Error) => {
getLogger().error(`Preload error in ${preloadPath}: `, error.message);
}
);
2021-09-29 18:37:30 +00:00
activeWindows.add(window);
window.on('closed', () => activeWindows.delete(window));
2022-07-05 16:44:53 +00:00
const setWindowFocus = () => {
window.webContents.send('set-window-focus', window.isFocused());
};
window.on('focus', setWindowFocus);
window.on('blur', setWindowFocus);
window.once('ready-to-show', setWindowFocus);
// This is a fallback in case we drop an event for some reason.
const focusInterval = setInterval(setWindowFocus, 10000);
window.on('closed', () => clearInterval(focusInterval));
2021-09-02 23:29:16 +00:00
// Works only for mainWindow because it has `enablePreferredSizeMode`
let lastZoomFactor = window.webContents.getZoomFactor();
const onZoomChanged = () => {
if (
window.isDestroyed() ||
!window.webContents ||
window.webContents.isDestroyed()
) {
return;
}
const zoomFactor = window.webContents.getZoomFactor();
2021-09-02 23:29:16 +00:00
if (lastZoomFactor === zoomFactor) {
return;
}
settingsChannel?.invokeCallbackInMainWindow('persistZoomFactor', [
zoomFactor,
]);
2021-09-02 23:29:16 +00:00
lastZoomFactor = zoomFactor;
};
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;
const DEFAULT_HEIGHT = 610;
// LARGEST_LEFT_PANE_WIDTH = 380
// TIMELINE_WIDTH = 300
// TIMELINE_MARGIN = 16 + 16
// 712 = LARGEST_LEFT_PANE_WIDTH + TIMELINE_WIDTH + TIMELINE_MARGIN
const MIN_WIDTH = 712;
const MIN_HEIGHT = 550;
const BOUNDS_BUFFER = 100;
2021-10-01 18:49:59 +00:00
type BoundsType = {
width: number;
height: number;
x: number;
y: number;
};
function isVisible(window: BoundsType, bounds: BoundsType) {
const boundsX = bounds?.x || 0;
const boundsY = bounds?.y || 0;
const boundsWidth = bounds?.width || DEFAULT_WIDTH;
const boundsHeight = bounds?.height || DEFAULT_HEIGHT;
// requiring BOUNDS_BUFFER pixels on the left or right side
2018-04-27 21:25:04 +00:00
const rightSideClearOfLeftBound =
window.x + window.width >= boundsX + BOUNDS_BUFFER;
const leftSideClearOfRightBound =
window.x <= boundsX + boundsWidth - BOUNDS_BUFFER;
// top can't be offscreen, and must show at least BOUNDS_BUFFER pixels at bottom
const topClearOfUpperBound = window.y >= boundsY;
2018-04-27 21:25:04 +00:00
const topClearOfLowerBound =
window.y <= boundsY + boundsHeight - BOUNDS_BUFFER;
2018-04-27 21:25:04 +00:00
return (
rightSideClearOfLeftBound &&
Auto-orient image attachments based on EXIF metadata As described in #998, images are sometimes displayed with an incorrect orientation. This is because cameras often write files in the native sensor byte order and attach the `Orientation` EXIF metadata to tell end-user devices how to display the images based on the original author’s capture orientation. Electron/Chromium (and therefore Signal Desktop) currently doesn’t support applying this metadata for `<img>` tags, e.g. CSS `image-orientation: from- image`. As a workaround, this change uses the `loadImage` library with the `orientation: true` flag to auto-orient images ~~before display~~ upon receipt and before sending. **Changes** - [x] ~~Auto-orient images during display in message list view~~ - [x] Ensure image is not displayed until loaded (to prevent layout reflow) . - [x] Auto-orient images upon receipt and before storing in IndexedDB (~~or preserve original data until Chromium offers native fix?~~) - [x] Auto-orient images in compose area preview. - [x] ~~Auto-orient images in lightbox view~~ - [x] Auto-orient images before sending / storage. - [x] Add EditorConfig for sharing code styles across editors. - [x] Fix ESLint ignore file. - [x] Change `function-paren-newline` ESLint rule from `consistent` to `multiline`. - [x] Add `operator-linebreak` ESLint rule for consistency. - [x] Added `blob-util` dependency for converting between array buffers, blobs, etc. - [x] Extracted `createMessageHandler` to consolidate logic for `onMessageReceived` and `onSentMessage`. - [x] Introduce `async` / `await` to simplify async coding (restore control flow for branching, loops, and exceptions). - [x] Introduce `window.Signal` namespace for exposing ES2015+ CommonJS modules. - [x] Introduce rudimentary `Message` and `Attachment` types to begin defining a schema and versioning. This will allow us to track which changes, e.g. auto-orient JPEGs, per message / attachment as well as which fields are stored. - [x] Renamed `window.dataURLtoBlob` to `window.dataURLToBlobSync` to both fix the strange `camelCase` as well as to highlight that this operation is synchronous and therefore blocks the user thread. - [x] Normalize all JPEG MIME types to `image/jpeg`, eliminating the invalid `image/jpg`. - [x] Add `npm run test-modules` command for testing non-browser specific CommonJS modules. - **Stretch Goals** - [ ] ~~Restrict `autoOrientImage` to `Blob` to narrow API interface.~~ Do this once we use PureScript. - [ ] ~~Return non-JPEGs as no-op from `autoOrientImage`.~~ Skipping `autoOrientImage` for non-JPEGs altogether. - [ ] Retroactively auto-orient existing JPEG image attachments in the background. --- Fixes #998 --- - **Blog:** EXIF Orientation Handling Is a Ghetto: https://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ - **Chromium Bug:** EXIF orientation is ignored: https://bugs.chromium.org/p/chromium/issues/detail?id=56845 - **Chromium Bug:** Support for the CSS image-orientation CSS property: https://bugs.chromium.org/p/chromium/issues/detail?id=158753 --- commit ce5090b473a2448229dc38e4c3f15d7ad0137714 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 10:35:36 2018 -0500 Inline message descriptors commit 329036e59c138c1e950ec7c654eebd7d87076de5 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:34:40 2018 -0500 Clarify order of operations Semantically, it makes more sense to do `getFile` before `clearForm` even though it seems to work either way. commit f9d4cfb2ba0d8aa308b0923bbe6066ea34cb97bd Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:18:26 2018 -0500 Simplify `operator-linebreak` configuration Enabling `before` caused more code changes and it turns out our previous configuration is already the default. commit db588997acdd90ed2ad829174ecbba744383c78b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:15:59 2018 -0500 Remove obsolete TODO commit 799c8817633f6afa0b731fc3b5434e463bd850e3 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:12:18 2018 -0500 Enable ESLint `function-paren-newline` `multiline` Per discussion. commit b660b6bc8ef41df7601a411213d6cda80821df87 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 15 17:10:48 2018 -0500 Use `messageDescriptor.id` not `source` commit 5e7309d176f4a7e97d3dc4c738e6b0ccd4792871 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 16:29:01 2018 -0500 Remove unnecessary `eslint-env` commit 393b3da55eabd7413596c86cc3971b063a0efe31 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 16:19:17 2018 -0500 Refactor `onSentMessage` and `onMessageReceived` Since they are so similar, we create the handlers using new `createMessageHandler` function. This allows us to ensure both synced and received messages go through schema upgrade pipeline. commit b3db0bf179c9a5bea96480cde28c6fa7193ac117 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 16:18:21 2018 -0500 Add `Message` descriptor functions commit 8febf125b1b42fe4ae1888dd50fcee2749dc1ff0 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 14:46:56 2018 -0500 Fix typo commit 98d951ef77bd578b313a4ff4b496b793e82e88d5 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 12:22:39 2018 -0500 Remove `promises` reference commit a0e9559ed5bed947dabf28cb672e63d39948d854 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 12:22:13 2018 -0500 Fix `AttachmentView::mediaType` fall-through commit 67be916a83951b8a1f9b22efe78a6da6b1825f38 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 12:03:41 2018 -0500 Remove minor TODOs commit 0af186e118256b62905de38487ffacc41693ff47 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:44:41 2018 -0500 Enable ESLint for `js/views/attachment_view.js` commit 28a2dc5b8a28e1a087924fdc7275bf7d9a577b92 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:44:12 2018 -0500 Remove dynamic type checks commit f4ce36fcfc2737de32d911cd6103f889097813f6 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:27:56 2018 -0500 Rename `process` to `upgradeSchema` - `Message.process` -> `Message.upgradeSchema` - `Attachment.process` -> `Attachment.upgradeSchema` - `Attachment::processVersion` -> `Attachment::schemaVersion` Document version history. commit 41b92c0a31050ba05ddb1c43171d651f3568b9ac Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:11:50 2018 -0500 Add `operator-linebreak` ESLint rule Based on the following discussion: https://github.com/signalapp/Signal-Desktop/pull/2040#discussion_r168029106 commit 462defbe55879060fe25bc69103d4429bae2b2f6 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 14 11:01:30 2018 -0500 Add missing `await` for `ConversationController.getOrCreateAndWait` Tested this by setting `if` condition to `true` and confirming it works. It turns rotating a profile key is more involved and might require registering a new account according to Matthew. commit c08058ee4b883b3e23a40683de802ac81ed74874 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 16:32:24 2018 -0500 Convert `FileList` to `Array` commit 70a6c4201925f57be1f94d9da3547fdefc7bbb53 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 15:46:34 2018 -0500 :art: Fix lint errors commit 2ca7cdbc31d4120d6c6a838a6dcf43bc209d9788 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 15:07:09 2018 -0500 Skip `autoOrientImage` for non-JPEG images commit 58eac383013c16ca363a4ed33dca5c7ba61284e5 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 14:55:35 2018 -0500 Move new-style modules to `window.Signal` namespace commit 02c9328877dce289d6116a18b1c223891bd3cd0b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 14:35:23 2018 -0500 Extract `npm run test-modules` command commit 2c708eb94fba468b81ea9427734896114f5a7807 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:25:51 2018 -0500 Extract `Message.process` commit 4a2e52f68a77536a0fa04aa3c29ad3e541a8fa7e Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:25:12 2018 -0500 Fix EditorConfig commit a346bab5db082720f5d47363f06301380e870425 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:13:02 2018 -0500 Remove `vim` directives on ESLint-ed files commit 7ec885c6359e495b407d5bc3eac9431d47c37fc6 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 13:08:24 2018 -0500 Remove CSP whitelisting of `blob:` We no longer use `autoOrientImage` using blob URLs. Bring this back if we decide to auto-orient legacy attachments. commit 879b6f58f4a3f4a9ed6915af6b1be46c1e90e0ca Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:57:05 2018 -0500 Use `Message` type to determine send function Throws on invalid message type. commit 5203d945c98fd2562ae4e22c5c9838d27dec305b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:56:48 2018 -0500 Whitelist `Whisper` global commit 8ad0b066a3690d3382b86bf6ac00c03df7d1e20b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:56:32 2018 -0500 Add `Whisper.Types` namespace This avoids namespace collision for `Whisper.Message`. commit 785a949fce2656ca7dcaf0869d6b9e0648114e80 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:55:43 2018 -0500 Add `Message` type commit 674a7357abf0dcc365455695d56c0479998ebf27 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:35:23 2018 -0500 Run ESLint on `Conversation::sendMessage` commit cd985aa700caa80946245b17ea1b856449f152a0 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:34:38 2018 -0500 Document type signature of `FileInputView::readFile` commit d70d70e52c49588a1dc9833dfe5dd7128e13607f Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:31:16 2018 -0500 Move attachment processing closer to sending This helps ensure processing happens uniformly, regardless of which code paths are taken to send an attachment. commit 532ac3e273a26b97f831247f9ee3412621b5c112 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:22:29 2018 -0500 Process attachment before it’s sent Picked this place since it already had various async steps, similar to `onMessageReceived` for the incoming `Attachment.process`. Could we try have this live closer to where we store it in IndexedDB, e.g. `Conversation::sendMessage`? commit a4582ae2fb6e1d3487131ba1f8fa6a00170cb32c Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 12:21:42 2018 -0500 Refactor `getFile` and `getFiles` Lint them using ESLint. commit 07e9114e65046d791fc4f6ed90d6e2e938ad559d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:37:31 2018 -0500 Document incoming and outgoing attachments fields Note how outgoing message attachments only have 4 fields. This presumably means the others are not used in our code and could be discarded for simplicity. commit fdc3ef289d6ec1be344a12d496839d5ba747bb6a Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:36:21 2018 -0500 Highlight that `dataURLToBlob` is synchronous commit b9c6bf600fcecedfd649ef2ae3c8629cced4e45a Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:35:49 2018 -0500 Add EditorConfig configuration commit e56101e229d56810c8e31ad7289043a152c6c449 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:34:23 2018 -0500 Replace custom with `blob-util` functions IMPORTANT: All of them are async so we need to use `await`, otherwise we get strange or silent errors. commit f95150f6a9569fabcb31f3acd9f6b7bf50b5d145 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:17:30 2018 -0500 Revert "Replace custom functions with `blob-util`" This reverts commit 8a81e9c01bfe80c0e1bf76737092206c06949512. commit 33860d93f3d30ec55c32f3f4a58729df2eb43f0d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:13:02 2018 -0500 Revert "Replace `blueimp-canvas-to-blob` with `blob-util`" This reverts commit 31b3e853e4afc78fe80995921aa4152d9f6e4783. commit 7a0ba6fed622d76a3c39c7f03de541a7edb5b8dd Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 11:12:58 2018 -0500 Replace `blueimp-canvas-to-blob` with `blob-util` commit 47a5f2bfd8b3f546e27e8d2b7e1969755d825a66 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:55:34 2018 -0500 Replace custom functions with `blob-util` commit 1cfa0efdb4fb1265369e2bf243c21f04f044fa01 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:47:02 2018 -0500 Add `blob-util` dependency commit 9ac26be1bd783cd5070d886de107dd3ad9c91ad1 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:46:44 2018 -0500 Document why we drop original image data during auto-orient commit 4136d6c382b99f41760a4da519d0db537fa7de8d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:46:27 2018 -0500 Extract `DEFAULT_JPEG_QUALITY` commit 4a7156327eb5f94dba80cb300b344ac591226b0e Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 10:37:11 2018 -0500 Drop support for invalid `image/jpg` MIME type commit 69fe96581f25413194032232f1bf704312e4754c Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:54:30 2018 -0500 Document `window.onInvalidStateError` global commit a48ba1c77458da38583ee9cd488f70a59f6ee0fd Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:54:04 2018 -0500 Selectively run ESLint on `js/background.js` Enabling ESLint on a per function basis allows us to incrementally improve the codebase without requiring large and potentially risky refactorings. commit e6d1cf826befc17ad4ec72fda8e761701665635e Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:16:23 2018 -0500 Move async attachment processing to `onMessageReceived` We previously processed attachments in `handleDataMessage` which is mostly a synchronous function, except for the saving of the model. Moving the processing into the already async `onMessageReceived` improves code clarity. commit be6ca2a9aae5b59c360817deb1e18d39d705755e Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:14:49 2018 -0500 Document import of ES2015+ modules commit eaaf7c41608fb988b8f4bbaa933cff110115610e Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:14:29 2018 -0500 :art: Fix lint error commit a25b0e2e3d0f72c6a7bf0a15683f02450d5209ee Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:13:57 2018 -0500 :art: Organize `require`s commit e0cc3d8fab6529d01b388acddf8605908c3d236b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 09:07:17 2018 -0500 Implement attachment process version Instead of keeping track of last normalization (processing) date, we now keep track of an internal processing version that will help us understand what kind of processing has already been completed for a given attachment. This will let us retroactively upgrade existing attachments. As we add more processing steps, we can build a processing pipeline that can convert any attachment processing version into a higher one, e.g. 4 -> 5 -> 6 -> 7. commit ad9083d0fdb880bc518e02251e51a39f7e1c585f Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:50:31 2018 -0500 Ignore ES2015+ files during JSCS linting commit 96641205f734927aaebc2342d977c555799c3e3b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:48:07 2018 -0500 Improve ESLint ignore rules Apparently, using unqualified `/**` patterns prevents `!` include patterns. Using qualified glob patterns, e.g. `js/models/**/*.js`, lets us work around this. commit 255e0ab15bd1a0ca8ca5746e42d23977c8765d01 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:44:59 2018 -0500 :abc: ESLint ignored files commit ebcb70258a26f234bd602072ac7c0a1913128132 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:35:47 2018 -0500 Whitelist `browser` environment for ESLint commit 3eaace6f3a21421c5aaaaf01592408c7ed83ecd3 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:35:05 2018 -0500 Use `MIME` module commit ba2cf7770e614415733414a2dcc48f110b929892 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:32:54 2018 -0500 :art: Fix lint errors commit 65acc86e8580e88f7a6611eb4b8fa5d7291f7a3f Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:30:42 2018 -0500 Add ES2015+ files to JSHint ignored list commit 8b6494ae6c9247acdfa059a9b361ec5ffcdb39f0 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:29:20 2018 -0500 Document potentially unexpected `autoScale` behavior commit 8b4c69b2002d1777d3621be10f92cbf432f9d4d6 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:26:47 2018 -0500 Test CommonJS modules separately Not sure how to test them as part of Grunt `unit-tests` task as `test/index.html` doesn’t allow for inclusion of CommonJS modules that use `require`. The tests are silently skipped. commit 213400e4b2bba3efee856a25b40e269221c3c39d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 13 08:24:27 2018 -0500 Add `MIME` type module commit 37a726e4fb4b3ed65914463122a5662847b5adee Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:18:05 2018 -0500 Return proper `Error` from `blobArrayToBuffer` commit 164752db5612220e4dcf58d57bcd682cb489a399 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:15:41 2018 -0500 :art: Fix ESLint errors commit d498dd79a067c75098dd3179814c914780e5cb4f Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:14:33 2018 -0500 Update `Attachment` type field definitions commit 141155a1533ff8fb616b70ea313432781bbebffd Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 20:12:50 2018 -0500 Move `blueimp-canvas-to-blob` from Bower to npm commit 7ccb833e5d286ddd6235d3e491c62ac1e4544510 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:33:50 2018 -0500 :art: Clarify data flow commit e7da41591fde5a830467bebf1b6f51c1f7293e74 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:31:21 2018 -0500 Use `blobUrl` for consistency commit 523a80eefe0e2858aa1fb2bb9539ec44da502963 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:28:06 2018 -0500 Remove just-in-time image auto-orient for lightbox We can bring this back if our users would like auto-orient for old attachments. commit 0739feae9c47dd523c10740d6cdf746d539f270c Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:27:21 2018 -0500 Remove just-in-time auto-orient of message attachments We can bring this back if our users would like auto-orient for old attachments. But better yet, we might implement this as database migration. commit ed43c66f92830ee233d5a94d0545eea4da43894d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:26:24 2018 -0500 Auto-orient JPEG attachments upon receipt commit e2eb8e36b017b048d57602fca14e45d657e0e1a1 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:25:26 2018 -0500 Expose `Attachment` type through `Whisper.Attachment` commit 9638fbc987b84f143ca34211dc4666d96248ea2f Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:23:39 2018 -0500 Use `contentType` from `model` commit 032c0ced46c3876cb9474b26f9d53d6f1c6b16a0 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:23:04 2018 -0500 Return `Error` object for `autoOrientImage` failures commit ff04bad8510c4b21aef350bed2b1887d0e055b98 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:22:32 2018 -0500 Add `options` for `autoOrientImage` output type / quality commit 87745b5586d1e182b51c9f9bc5e4eaf6dbc16722 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:18:46 2018 -0500 Add `Attachment` type Defines various functions on attachments, e.g. normalization (auto-orient JPEGs, etc.) commit de27fdc10a53bc8882a9c978e82265db9ac6d6f5 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 16:16:34 2018 -0500 Add `yarn grunt` shortcut This allows us to use local `grunt-cli` for `grunt dev`. commit 59974db5a5da0d8f4cdc8ce5c4e3c974ecd5e754 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 10:10:11 2018 -0500 Improve readability commit b5ba96f1e6f40f2e1fa77490c583217768e1f412 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 10:08:12 2018 -0500 Use `snake_case` for module names Prevents problems across case-sensitive and case-insensitive file systems. We can work around this in the future using a lint rule such as `eslint-plugin-require-path-exists`. See discussion: https://github.com/signalapp/Signal-Desktop/pull/2040#discussion_r167365931 commit 48c5d3155c96ef628b00d99b52975e580d1d5501 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 12 10:05:44 2018 -0500 :art: Use destructuring commit 4822f49f22382a99ebf142b337375f7c25251d76 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:41:40 2018 -0500 Auto-orient images in lightbox view commit 7317110809677dddbbef3fadbf912cdba1c010bf Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:40:14 2018 -0500 Document magic number for escape key commit c790d07389a7d0bbf5298de83dbcfa8be1e7696b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:38:35 2018 -0500 Make second `View` argument an `options` object commit fbe010bb63d0088af9dfe11f153437fab34247e0 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 17:27:40 2018 -0500 Allow `loadImage` to fetch `blob://` URLs commit ec35710d002b019a273eeb48f94dcaf2babe5d96 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:57:48 2018 -0500 :art: Shorten `autoOrientImage` import commit d07433e3cf316c6a143a0c9393ba26df9e3af17b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:57:19 2018 -0500 Make `autoOrientImage` module standalone commit c285bf5e33cdf10e0ef71e72cd6f55aef0df96ef Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:55:44 2018 -0500 Replace `loadImage` with `autoOrientImage` commit 44318549235af01fd061c25f557c93fd21cebb7a Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:53:23 2018 -0500 Add `autoOrientImage` module This module exposes `loadImage` with a `Promise` based interface and pre- populates `orientation: true` option to auto-orient input. Returns data URL as string. The module uses a named export as refactoring references of modules with `default` (`module.exports`) export references can be error-prone. See: https://basarat.gitbooks.io/typescript/docs/tips/defaultIsBad.html commit c77063afc6366fe49615052796fe46f9b369de39 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:44:30 2018 -0500 Auto-orient preview images See: #998 commit 06dba5eb8f662c11af3a9ba8395bb453ab2e5f8d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:43:23 2018 -0500 TODO: Use native `Canvas::toBlob` One challenge is that `Canvas::toBlob` is async whereas `dataURLtoBlob` is sync. commit b15c304a3125dd023fd90990e6225a7303f3596f Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 16:42:45 2018 -0500 Make `null` check strict Appeases JSHint. ESLint has a nice `smart` option for `eqeqeq` rule: https://eslint.org/docs/rules/eqeqeq#smart commit ea70b92d9b18201758e11fdc25b09afc97b50055 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 15:23:58 2018 -0500 Use `Canvas::toDataURL` to preserve `ImageView` logic This way, all the other code paths remain untouched in case we want to remove the auto-orient code once Chrome supports the `image-orientation` CSS property. See: - #998 - https://developer.mozilla.org/en-US/docs/Web/CSS/image-orientation commit 62fd744f9f27d951573a68d2cdfe7ba2a3784b41 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:38:04 2018 -0500 Use CSS to constrain auto-oriented images commit f4d3392687168c237441b29140c7968b49dbef9e Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:35:02 2018 -0500 Replace `ImageView` `el` with auto-oriented `canvas` See: #998 commit 1602d7f610e4993ad1291f88197f9ead1e25e776 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:25:48 2018 -0500 Pass `Blob` to `View` (for `ImageView`) This allows us to do JPEG auto-orientation based on EXIF metadata. commit e6a414f2b2a80da1137b839b348a38510efd04bb Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 14:25:12 2018 -0500 :hocho: Remove newline commit 5f0d9570d7862fc428ff89c2ecfd332a744537e5 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 11:17:02 2018 -0500 Expose `blueimp-load-image` as `window.loadImage` commit 1e1c62fe2f6a76dbcf1998dd468c26187c9871dc Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 11:16:46 2018 -0500 Add `blueimp-load-image` npm dependency commit ad17fa8a68a21ca5ddec336801b8568009bef3d4 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 9 11:14:40 2018 -0500 Remove `blueimp-load-image` Bower dependency
2018-02-21 15:26:59 +00:00
leftSideClearOfRightBound &&
topClearOfUpperBound &&
2018-04-27 21:25:04 +00:00
topClearOfLowerBound
);
}
2021-10-01 18:49:59 +00:00
let windowIcon: string;
2020-04-20 21:07:16 +00:00
2021-02-01 20:01:25 +00:00
if (OS.isWindows()) {
2021-10-01 18:49:59 +00:00
windowIcon = join(__dirname, '../build/icons/win/icon.ico');
2021-02-01 20:01:25 +00:00
} else if (OS.isLinux()) {
2021-10-01 18:49:59 +00:00
windowIcon = join(__dirname, '../images/signal-logo-desktop-linux.png');
2020-04-20 21:07:16 +00:00
} else {
2021-10-01 18:49:59 +00:00
windowIcon = join(__dirname, '../build/icons/png/512x512.png');
2020-04-20 21:07:16 +00:00
}
const mainTitleBarStyle =
OS.isLinux() || isTestEnvironment(getEnvironment())
? ('default' as const)
: ('hidden' as const);
2022-07-05 16:44:53 +00:00
const nonMainTitleBarStyle = OS.hasCustomTitleBar()
? ('hidden' as const)
: ('default' as const);
async function getTitleBarOverlay(): Promise<TitleBarOverlayOptions | false> {
2022-07-05 16:44:53 +00:00
if (!OS.hasCustomTitleBar()) {
return false;
}
const theme = await getResolvedThemeSetting();
let color: string;
let symbolColor: string;
if (theme === 'light') {
color = '#e8e8e8';
symbolColor = '#1b1b1b';
} else if (theme === 'dark') {
// $color-gray-80
color = '#2e2e2e';
// $color-gray-05
symbolColor = '#e9e9e9';
} else {
throw missingCaseError(theme);
}
return {
color,
symbolColor,
2022-06-23 15:40:36 +00:00
// Should match stylesheets/components/TitleBarContainer.scss
2022-06-24 00:40:45 +00:00
height: 28 - 1,
};
}
2020-03-20 21:00:11 +00:00
async function createWindow() {
const usePreloadBundle =
!isTestEnvironment(getEnvironment()) || forcePreloadBundle;
const titleBarOverlay = await getTitleBarOverlay();
2021-10-01 18:49:59 +00:00
const windowOptions: Electron.BrowserWindowConstructorOptions = {
show: false,
2020-09-09 00:46:29 +00:00
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT,
autoHideMenuBar: false,
titleBarStyle: mainTitleBarStyle,
titleBarOverlay,
2021-10-01 18:49:59 +00:00
backgroundColor: isTestEnvironment(getEnvironment())
2021-07-28 23:46:25 +00:00
? '#ffffff' // Tests should always be rendered on a white background
2022-05-11 22:58:14 +00:00
: await getBackgroundColor(),
2020-09-09 00:46:29 +00:00
webPreferences: {
...defaultWebPrefs,
2020-09-09 00:46:29 +00:00
nodeIntegration: false,
nodeIntegrationInWorker: false,
contextIsolation: false,
2021-10-01 18:49:59 +00:00
preload: join(
2021-04-07 22:40:12 +00:00
__dirname,
usePreloadBundle
? '../preload.bundle.js'
: '../ts/windows/main/preload.js'
2021-04-07 22:40:12 +00:00
),
2020-09-09 00:46:29 +00:00
spellcheck: await getSpellCheckSetting(),
backgroundThrottling: isThrottlingEnabled,
2021-09-02 23:29:16 +00:00
enablePreferredSizeMode: true,
2022-05-12 19:41:37 +00:00
disableBlinkFeatures: 'Accelerated2dCanvas,AcceleratedSmallCanvases',
},
2020-09-09 00:46:29 +00:00
icon: windowIcon,
2021-10-01 18:49:59 +00:00
...pick(windowConfig, ['autoHideMenuBar', 'width', 'height', 'x', 'y']),
2020-09-09 00:46:29 +00:00
};
2021-10-01 18:49:59 +00:00
if (!isNumber(windowOptions.width) || windowOptions.width < MIN_WIDTH) {
windowOptions.width = DEFAULT_WIDTH;
}
2021-10-01 18:49:59 +00:00
if (!isNumber(windowOptions.height) || windowOptions.height < MIN_HEIGHT) {
windowOptions.height = DEFAULT_HEIGHT;
}
2021-10-01 18:49:59 +00:00
if (!isBoolean(windowOptions.autoHideMenuBar)) {
delete windowOptions.autoHideMenuBar;
}
const startInTray =
(await systemTraySettingCache.get()) ===
SystemTraySetting.MinimizeToAndStartInSystemTray;
2021-10-01 18:49:59 +00:00
const visibleOnAnyScreen = some(screen.getAllDisplays(), display => {
if (
isNumber(windowOptions.x) &&
isNumber(windowOptions.y) &&
isNumber(windowOptions.width) &&
isNumber(windowOptions.height)
) {
return isVisible(windowOptions as BoundsType, get(display, 'bounds'));
}
2021-10-01 18:49:59 +00:00
getLogger().error(
"visibleOnAnyScreen: windowOptions didn't have valid bounds fields"
);
return false;
});
if (!visibleOnAnyScreen) {
2021-10-01 18:49:59 +00:00
getLogger().info('Location reset needed');
delete windowOptions.x;
delete windowOptions.y;
}
2021-10-01 18:49:59 +00:00
getLogger().info(
'Initializing BrowserWindow config:',
2018-04-27 21:25:04 +00:00
JSON.stringify(windowOptions)
);
// Create the browser window.
mainWindow = new BrowserWindow(windowOptions);
2021-10-01 18:49:59 +00:00
if (settingsChannel) {
settingsChannel.setMainWindow(mainWindow);
}
2021-08-18 20:08:14 +00:00
mainWindowCreated = true;
setupSpellChecker(mainWindow, getLocale());
2021-07-06 19:28:27 +00:00
if (!startInTray && windowConfig && windowConfig.maximized) {
mainWindow.maximize();
}
if (!startInTray && windowConfig && windowConfig.fullscreen) {
mainWindow.setFullScreen(true);
}
if (systemTrayService) {
systemTrayService.setMainWindow(mainWindow);
}
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;
}
const size = mainWindow.getSize();
const position = mainWindow.getPosition();
const newWindowConfig = {
maximized: mainWindow.isMaximized(),
autoHideMenuBar: mainWindow.autoHideMenuBar,
fullscreen: mainWindow.isFullScreen(),
width: size[0],
height: size[1],
x: position[0],
y: position[1],
};
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;
if (!windowState.requestedShutdown()) {
debouncedSaveStats();
}
}
mainWindow.on('resize', captureWindowStats);
mainWindow.on('move', captureWindowStats);
2021-10-01 18:49:59 +00:00
if (getEnvironment() === Environment.Test) {
mainWindow.loadURL(await prepareFileUrl([__dirname, '../test/index.html']));
} else {
mainWindow.loadURL(await prepareFileUrl([__dirname, '../background.html']));
}
2021-10-01 18:49:59 +00:00
if (!enableCI && config.get<boolean>('openDevTools')) {
2017-04-05 18:42:30 +00:00
// Open the DevTools.
mainWindow.webContents.openDevTools();
2017-04-05 18:42:30 +00:00
}
handleCommonWindowEvents(mainWindow, titleBarOverlay);
2020-06-04 18:16:19 +00:00
// App dock icon bounce
bounce.init(mainWindow);
// Emitted when the window is about to be closed.
// Note: We do most of our shutdown logic here because all windows are closed by
// Electron before the app quits.
mainWindow.on('close', async e => {
2021-10-01 18:49:59 +00:00
if (!mainWindow) {
getLogger().info('close event: no main window');
return;
}
getLogger().info('close event', {
readyForShutdown: windowState.readyForShutdown(),
shouldQuit: windowState.shouldQuit(),
});
// If the application is terminating, just do the default
2018-04-27 21:25:04 +00:00
if (
2021-10-01 18:49:59 +00:00
isTestEnvironment(getEnvironment()) ||
(windowState.readyForShutdown() && windowState.shouldQuit())
2018-04-27 21:25:04 +00:00
) {
return;
}
// Prevent the shutdown
e.preventDefault();
/**
* if the user is in fullscreen mode and closes the window, not the
* application, we need them leave fullscreen first before closing it to
* prevent a black screen.
*
* issue: https://github.com/signalapp/Signal-Desktop/issues/4348
*/
if (mainWindow.isFullScreen()) {
2021-10-01 18:49:59 +00:00
mainWindow.once('leave-full-screen', () => mainWindow?.hide());
2020-07-01 18:05:41 +00:00
mainWindow.setFullScreen(false);
} else {
2020-07-01 18:05:41 +00:00
mainWindow.hide();
}
// On Mac, or on other platforms when the tray icon is in use, the window
// should be only hidden, not closed, when the user clicks the close button
const usingTrayIcon = shouldMinimizeToSystemTray(
await systemTraySettingCache.get()
);
2021-02-01 20:01:25 +00:00
if (!windowState.shouldQuit() && (usingTrayIcon || OS.isMacOS())) {
return;
}
windowState.markRequestedShutdown();
await requestShutdown();
2021-10-01 18:49:59 +00:00
windowState.markReadyForShutdown();
await sql.close();
app.quit();
});
// Emitted when the window is closed.
mainWindow.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = undefined;
2021-10-01 18:49:59 +00:00
if (settingsChannel) {
settingsChannel.setMainWindow(mainWindow);
}
if (systemTrayService) {
systemTrayService.setMainWindow(mainWindow);
}
});
2021-02-01 20:01:25 +00:00
mainWindow.on('enter-full-screen', () => {
2021-10-01 18:49:59 +00:00
if (mainWindow) {
mainWindow.webContents.send('full-screen-change', true);
}
2021-02-01 20:01:25 +00:00
});
mainWindow.on('leave-full-screen', () => {
2021-10-01 18:49:59 +00:00
if (mainWindow) {
mainWindow.webContents.send('full-screen-change', false);
}
2021-02-01 20:01:25 +00:00
});
mainWindow.once('ready-to-show', async () => {
2021-10-01 18:49:59 +00:00
getLogger().info('main window is ready-to-show');
// Ignore sql errors and show the window anyway
await sqlInitPromise;
if (!mainWindow) {
return;
}
const shouldShowWindow =
!app.getLoginItemSettings().wasOpenedAsHidden && !startInTray;
if (shouldShowWindow) {
2021-10-01 18:49:59 +00:00
getLogger().info('showing main window');
mainWindow.show();
}
});
}
// Renderer asks if we are done with the database
ipc.on('database-ready', async event => {
2021-10-01 18:49:59 +00:00
if (!sqlInitPromise) {
getLogger().error('database-ready requested, but sqlInitPromise is falsey');
return;
}
const { error } = await sqlInitPromise;
if (error) {
2021-10-01 18:49:59 +00:00
getLogger().error(
'database-ready requested, but got sql error',
error && error.stack
);
return;
}
2021-10-01 18:49:59 +00:00
getLogger().info('sending `database-ready`');
event.sender.send('database-ready');
});
ipc.on('show-window', () => {
showWindow();
});
2021-02-01 20:01:25 +00:00
ipc.on('title-bar-double-click', () => {
if (!mainWindow) {
return;
}
if (OS.isMacOS()) {
switch (
systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string')
) {
case 'Minimize':
mainWindow.minimize();
break;
case 'Maximize':
toggleMaximizedBrowserWindow(mainWindow);
break;
default:
// If this is disabled, it'll be 'None'. If it's anything else, that's unexpected,
// but we'll just no-op.
break;
}
} else {
// This is currently only supported on macOS. This `else` branch is just here when/if
// we add support for other operating systems.
toggleMaximizedBrowserWindow(mainWindow);
}
});
ipc.on('set-is-call-active', (_event, isCallActive) => {
2021-12-10 22:53:10 +00:00
preventDisplaySleepService.setEnabled(isCallActive);
if (!mainWindow) {
return;
}
if (!isThrottlingEnabled) {
return;
}
2021-10-01 18:49:59 +00:00
let backgroundThrottling: boolean;
if (isCallActive) {
2021-10-01 18:49:59 +00:00
getLogger().info('Background throttling disabled because a call is active');
backgroundThrottling = false;
} else {
2021-10-01 18:49:59 +00:00
getLogger().info('Background throttling enabled because no call is active');
backgroundThrottling = true;
}
mainWindow.webContents.setBackgroundThrottling(backgroundThrottling);
});
2021-08-09 20:06:21 +00:00
ipc.on('convert-image', async (event, uuid, data) => {
const { error, response } = await heicConverter(uuid, data);
event.reply(`convert-image:${uuid}`, { error, response });
});
let isReadyForUpdates = false;
async function readyForUpdates() {
if (isReadyForUpdates) {
return;
}
isReadyForUpdates = true;
// First, install requested sticker pack
2020-08-27 19:44:51 +00:00
const incomingHref = getIncomingHref(process.argv);
if (incomingHref) {
handleSgnlHref(incomingHref);
}
// Second, start checking for app updates
try {
strictAssert(
settingsChannel !== undefined,
'SettingsChannel must be initialized'
);
await updater.start(settingsChannel, getLogger(), getMainWindow);
} catch (error) {
2021-10-01 18:49:59 +00:00
getLogger().error(
'Error starting update checks:',
error && error.stack ? error.stack : error
);
}
}
2021-06-30 21:27:18 +00:00
async function forceUpdate() {
try {
2021-10-01 18:49:59 +00:00
getLogger().info('starting force update');
2021-06-30 21:27:18 +00:00
await updater.force();
} catch (error) {
2021-10-01 18:49:59 +00:00
getLogger().error(
2021-06-30 21:27:18 +00:00
'Error during force update:',
error && error.stack ? error.stack : error
);
}
}
ipc.once('ready-for-updates', readyForUpdates);
const TEN_MINUTES = 10 * 60 * 1000;
setTimeout(readyForUpdates, TEN_MINUTES);
2020-06-10 16:56:10 +00:00
function openContactUs() {
shell.openExternal(createSupportUrl({ locale: app.getLocale() }));
2020-06-10 16:56:10 +00:00
}
function openJoinTheBeta() {
// If we omit the language, the site will detect the language and redirect
2020-06-10 16:56:10 +00:00
shell.openExternal('https://support.signal.org/hc/articles/360007318471');
}
function openReleaseNotes() {
if (mainWindow && mainWindow.isVisible()) {
mainWindow.webContents.send('show-release-notes');
return;
}
2018-04-27 21:25:04 +00:00
shell.openExternal(
`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`
);
}
function openSupportPage() {
// If we omit the language, the site will detect the language and redirect
shell.openExternal('https://support.signal.org/hc/sections/360001602812');
}
function openForums() {
shell.openExternal('https://community.signalusers.org/');
}
2019-11-07 21:36:16 +00:00
function showKeyboardShortcuts() {
if (mainWindow) {
mainWindow.webContents.send('show-keyboard-shortcuts');
}
}
New design for import/install, 'light' import (#2053) - A new design for the import flow. It features: - Icons at the top of every screen - Gray background, blue buttons, thinner text - Simpler copy - A new design for the install flow. It features: - Immediate entry into the QR code screen - Animated dots to show that we're loading the QR code from the server - Fewer screens: 1) QR 2) device name 3) sync-in-progress - When not set up, the app opens directly into the install screen, which has been streamlined. The `--import` command-line argument will cause the app to open directly into the import flow. - Support for two different flavors of builds - the normal build will open into the standard registration flow, and the import flavor will be exactly the same except during setup it will open directly into the import flow. - A new design for the (dev-only) standalone registration view - When these install sequences are active, the OS File menu has entries to allow you to switch the method of setup you'd like to use. These go away as soon as the first step is taken in any of these flows. - The device name (chosen on initial setup) is now shown in the settings panel - At the end of a light import, we hand off to the normal device link screen, starting at the QR code. On a full import, we remove the sensitive encryption information in the export to prevent conflicts on multiple imports. - `Whisper.Backup.exportToDirectory()` takes an options object so you can tell it to do a light export. - `Whisper.Backup.importFromDirectory()` takes an options object so you can force it to load only the light components found on disk. It also returns an object so you can tell whether a given import was a full import or light import. - On start of import, we build a list of all the ids present in the messages, conversations, and groups stores in IndexedDB. This can take some time if a lot of data is in the database already, but it makes the subsequent deduplicated import very fast. - Disappearing messages are now excluded when exporting - Remove some TODOs in the tests
2018-02-22 18:40:32 +00:00
function setupAsNewDevice() {
if (mainWindow) {
mainWindow.webContents.send('set-up-as-new-device');
}
}
function setupAsStandalone() {
if (mainWindow) {
mainWindow.webContents.send('set-up-as-standalone');
}
}
2021-10-01 18:49:59 +00:00
let screenShareWindow: BrowserWindow | undefined;
async function showScreenShareWindow(sourceName: string) {
if (screenShareWindow) {
screenShareWindow.showInactive();
return;
}
const width = 480;
const display = screen.getPrimaryDisplay();
const options = {
alwaysOnTop: true,
autoHideMenuBar: true,
backgroundColor: '#2e2e2e',
darkTheme: true,
frame: false,
fullscreenable: false,
height: 44,
maximizable: false,
minimizable: false,
resizable: false,
show: false,
2021-10-01 18:49:59 +00:00
title: getLocale().i18n('screenShareWindow'),
titleBarStyle: nonMainTitleBarStyle,
width,
webPreferences: {
...defaultWebPrefs,
nodeIntegration: false,
nodeIntegrationInWorker: false,
contextIsolation: true,
2021-10-01 18:49:59 +00:00
preload: join(__dirname, '../ts/windows/screenShare/preload.js'),
},
x: Math.floor(display.size.width / 2) - width / 2,
y: 24,
};
screenShareWindow = new BrowserWindow(options);
handleCommonWindowEvents(screenShareWindow);
screenShareWindow.loadURL(
await prepareFileUrl([__dirname, '../screenShare.html'])
);
screenShareWindow.on('closed', () => {
2021-10-01 18:49:59 +00:00
screenShareWindow = undefined;
});
screenShareWindow.once('ready-to-show', () => {
2021-10-01 18:49:59 +00:00
if (screenShareWindow) {
screenShareWindow.showInactive();
screenShareWindow.webContents.send(
'render-screen-sharing-controller',
sourceName
);
}
});
}
2021-10-01 18:49:59 +00:00
let aboutWindow: BrowserWindow | undefined;
2022-05-11 22:58:14 +00:00
async function showAbout() {
if (aboutWindow) {
aboutWindow.show();
return;
}
const titleBarOverlay = await getTitleBarOverlay();
const options = {
width: 500,
height: 500,
resizable: false,
2021-10-01 18:49:59 +00:00
title: getLocale().i18n('aboutSignalDesktop'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true,
2022-05-11 22:58:14 +00:00
backgroundColor: await getBackgroundColor(),
show: false,
webPreferences: {
...defaultWebPrefs,
nodeIntegration: false,
nodeIntegrationInWorker: false,
contextIsolation: true,
2021-10-01 18:49:59 +00:00
preload: join(__dirname, '../ts/windows/about/preload.js'),
nativeWindowOpen: true,
},
};
aboutWindow = new BrowserWindow(options);
handleCommonWindowEvents(aboutWindow, titleBarOverlay);
aboutWindow.loadURL(await prepareFileUrl([__dirname, '../about.html']));
aboutWindow.on('closed', () => {
2021-10-01 18:49:59 +00:00
aboutWindow = undefined;
});
aboutWindow.once('ready-to-show', () => {
2021-10-01 18:49:59 +00:00
if (aboutWindow) {
aboutWindow.show();
}
});
}
2021-10-01 18:49:59 +00:00
let settingsWindow: BrowserWindow | undefined;
2022-05-11 22:58:14 +00:00
async function showSettingsWindow() {
if (settingsWindow) {
settingsWindow.show();
return;
}
2019-10-17 18:22:07 +00:00
const titleBarOverlay = await getTitleBarOverlay();
const options = {
2021-08-18 20:08:14 +00:00
width: 700,
height: 700,
frame: true,
resizable: false,
2021-10-01 18:49:59 +00:00
title: getLocale().i18n('signalDesktopPreferences'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true,
2022-05-11 22:58:14 +00:00
backgroundColor: await getBackgroundColor(),
show: false,
webPreferences: {
...defaultWebPrefs,
nodeIntegration: false,
nodeIntegrationInWorker: false,
contextIsolation: true,
2021-10-01 18:49:59 +00:00
preload: join(__dirname, '../ts/windows/settings/preload.js'),
nativeWindowOpen: true,
},
};
settingsWindow = new BrowserWindow(options);
handleCommonWindowEvents(settingsWindow, titleBarOverlay);
settingsWindow.loadURL(await prepareFileUrl([__dirname, '../settings.html']));
settingsWindow.on('closed', () => {
2021-10-01 18:49:59 +00:00
settingsWindow = undefined;
});
ipc.once('settings-done-rendering', () => {
if (!settingsWindow) {
2021-10-01 18:49:59 +00:00
getLogger().warn('settings-done-rendering: no settingsWindow available!');
return;
2021-08-18 20:08:14 +00:00
}
settingsWindow.show();
});
}
2019-12-17 20:25:57 +00:00
async function getIsLinked() {
try {
const number = await sql.sqlCall('getItemById', ['number_id']);
const password = await sql.sqlCall('getItemById', ['password']);
2019-12-17 20:25:57 +00:00
return Boolean(number && password);
} catch (e) {
return false;
}
}
2021-10-01 18:49:59 +00:00
let stickerCreatorWindow: BrowserWindow | undefined;
2019-12-17 20:25:57 +00:00
async function showStickerCreator() {
2020-01-08 17:44:54 +00:00
if (!(await getIsLinked())) {
2021-10-01 18:49:59 +00:00
const message = getLocale().i18n('StickerCreator--Authentication--error');
2019-12-17 20:25:57 +00:00
dialog.showMessageBox({
type: 'warning',
message,
});
return;
}
if (stickerCreatorWindow) {
stickerCreatorWindow.show();
return;
}
const { x = 0, y = 0 } = windowConfig || {};
const titleBarOverlay = await getTitleBarOverlay();
2019-12-17 20:25:57 +00:00
const options = {
x: x + 100,
y: y + 100,
width: 800,
minWidth: 800,
height: 650,
2021-10-01 18:49:59 +00:00
title: getLocale().i18n('signalDesktopStickerCreator'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
2019-12-17 20:25:57 +00:00
autoHideMenuBar: true,
2022-05-11 22:58:14 +00:00
backgroundColor: await getBackgroundColor(),
2019-12-17 20:25:57 +00:00
show: false,
webPreferences: {
...defaultWebPrefs,
2019-12-17 20:25:57 +00:00
nodeIntegration: false,
nodeIntegrationInWorker: false,
contextIsolation: false,
2021-10-01 18:49:59 +00:00
preload: join(__dirname, '../sticker-creator/preload.js'),
2019-12-17 20:25:57 +00:00
nativeWindowOpen: true,
2020-03-20 21:00:11 +00:00
spellcheck: await getSpellCheckSetting(),
2019-12-17 20:25:57 +00:00
},
};
stickerCreatorWindow = new BrowserWindow(options);
setupSpellChecker(stickerCreatorWindow, getLocale());
2019-12-17 20:25:57 +00:00
handleCommonWindowEvents(stickerCreatorWindow, titleBarOverlay);
2019-12-17 20:25:57 +00:00
2021-10-01 18:49:59 +00:00
const appUrl = process.env.SIGNAL_ENABLE_HTTP
? prepareUrl(
new URL('http://localhost:6380/sticker-creator/dist/index.html')
)
2021-10-01 18:49:59 +00:00
: prepareFileUrl([__dirname, '../sticker-creator/dist/index.html']);
2019-12-17 20:25:57 +00:00
stickerCreatorWindow.loadURL(await appUrl);
2019-12-17 20:25:57 +00:00
stickerCreatorWindow.on('closed', () => {
2021-10-01 18:49:59 +00:00
stickerCreatorWindow = undefined;
2019-12-17 20:25:57 +00:00
});
stickerCreatorWindow.once('ready-to-show', () => {
2021-10-01 18:49:59 +00:00
if (!stickerCreatorWindow) {
return;
}
2019-12-17 20:25:57 +00:00
stickerCreatorWindow.show();
2021-10-01 18:49:59 +00:00
if (config.get<boolean>('openDevTools')) {
2019-12-17 20:25:57 +00:00
// Open the DevTools.
stickerCreatorWindow.webContents.openDevTools();
}
});
}
2021-10-01 18:49:59 +00:00
let debugLogWindow: BrowserWindow | undefined;
async function showDebugLogWindow() {
if (debugLogWindow) {
debugLogWindow.show();
return;
}
const titleBarOverlay = await getTitleBarOverlay();
const options = {
2021-10-15 18:11:59 +00:00
width: 700,
height: 500,
resizable: false,
2021-10-01 18:49:59 +00:00
title: getLocale().i18n('debugLog'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true,
2022-05-11 22:58:14 +00:00
backgroundColor: await getBackgroundColor(),
show: false,
webPreferences: {
...defaultWebPrefs,
nodeIntegration: false,
nodeIntegrationInWorker: false,
contextIsolation: true,
preload: join(__dirname, '../ts/windows/debuglog/preload.js'),
nativeWindowOpen: true,
},
parent: mainWindow,
// Electron has [a macOS bug][0] that causes parent windows to become unresponsive if
// it's fullscreen and opens a fullscreen child window. Until that's fixed, we
// prevent the child window from being fullscreenable, which sidesteps the problem.
// [0]: https://github.com/electron/electron/issues/32374
fullscreenable: !OS.isMacOS(),
};
debugLogWindow = new BrowserWindow(options);
handleCommonWindowEvents(debugLogWindow, titleBarOverlay);
debugLogWindow.loadURL(
await prepareFileUrl([__dirname, '../debug_log.html'])
);
debugLogWindow.on('closed', () => {
2021-10-01 18:49:59 +00:00
debugLogWindow = undefined;
});
debugLogWindow.once('ready-to-show', () => {
2021-10-01 18:49:59 +00:00
if (debugLogWindow) {
debugLogWindow.show();
// Electron sometimes puts the window in a strange spot until it's shown.
debugLogWindow.center();
2021-10-01 18:49:59 +00:00
}
});
}
2021-10-01 18:49:59 +00:00
let permissionsPopupWindow: BrowserWindow | undefined;
function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) {
2020-09-09 00:46:29 +00:00
// eslint-disable-next-line no-async-promise-executor
return new Promise<void>(async (resolveFn, reject) => {
2020-06-04 18:16:19 +00:00
if (permissionsPopupWindow) {
permissionsPopupWindow.show();
reject(new Error('Permission window already showing'));
2021-10-01 18:49:59 +00:00
return;
2020-06-04 18:16:19 +00:00
}
if (!mainWindow) {
reject(new Error('No main window'));
2021-10-01 18:49:59 +00:00
return;
2020-06-04 18:16:19 +00:00
}
2020-06-04 18:16:19 +00:00
const size = mainWindow.getSize();
const options = {
width: Math.min(400, size[0]),
height: Math.min(150, size[1]),
resizable: false,
2021-10-01 18:49:59 +00:00
title: getLocale().i18n('allowAccess'),
titleBarStyle: nonMainTitleBarStyle,
2020-06-04 18:16:19 +00:00
autoHideMenuBar: true,
2022-05-11 22:58:14 +00:00
backgroundColor: await getBackgroundColor(),
2020-06-04 18:16:19 +00:00
show: false,
modal: true,
webPreferences: {
...defaultWebPrefs,
2020-06-04 18:16:19 +00:00
nodeIntegration: false,
nodeIntegrationInWorker: false,
2021-09-17 22:24:21 +00:00
contextIsolation: true,
2021-10-01 18:49:59 +00:00
preload: join(__dirname, '../ts/windows/permissions/preload.js'),
2020-06-04 18:16:19 +00:00
nativeWindowOpen: true,
},
parent: mainWindow,
};
permissionsPopupWindow = new BrowserWindow(options);
2020-06-04 18:16:19 +00:00
handleCommonWindowEvents(permissionsPopupWindow);
2020-06-04 18:16:19 +00:00
permissionsPopupWindow.loadURL(
await prepareFileUrl([__dirname, '../permissions_popup.html'], {
2020-06-04 18:16:19 +00:00
forCalling,
forCamera,
})
);
2020-06-04 18:16:19 +00:00
permissionsPopupWindow.on('closed', () => {
removeDarkOverlay();
2021-10-01 18:49:59 +00:00
permissionsPopupWindow = undefined;
resolveFn();
2020-06-04 18:16:19 +00:00
});
permissionsPopupWindow.once('ready-to-show', () => {
2021-10-01 18:49:59 +00:00
if (permissionsPopupWindow) {
addDarkOverlay();
permissionsPopupWindow.show();
}
2020-06-04 18:16:19 +00:00
});
});
}
const runSQLCorruptionHandler = async () => {
// This is a glorified event handler. Normally, this promise never resolves,
// but if there is a corruption error triggered by any query that we run
// against the database - the promise will resolve and we will call
// `onDatabaseError`.
const error = await sql.whenCorrupted();
getLogger().error(
'Detected sql corruption in main process. ' +
`Restarting the application immediately. Error: ${error.message}`
);
await onDatabaseError(error.stack || error.message);
};
async function initializeSQL(
userDataPath: string
): Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> {
2021-10-01 18:49:59 +00:00
let key: string | undefined;
const keyFromConfig = userConfig.get('key');
if (typeof keyFromConfig === 'string') {
key = keyFromConfig;
} else if (keyFromConfig) {
getLogger().warn(
"initializeSQL: got key from config, but it wasn't a string"
);
}
if (!key) {
2021-10-01 18:49:59 +00:00
getLogger().info(
'key/initialize: Generating new encryption key, since we did not find it on disk'
);
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
2021-10-01 18:49:59 +00:00
key = randomBytes(32).toString('hex');
userConfig.set('key', key);
}
sqlInitTimeStart = Date.now();
try {
// This should be the first awaited call in this function, otherwise
// `sql.sqlCall` will throw an uninitialized error instead of waiting for
// init to finish.
await sql.initialize({
configDir: userDataPath,
key,
2021-10-01 18:49:59 +00:00
logger: getLogger(),
});
2021-10-01 18:49:59 +00:00
} catch (error: unknown) {
if (error instanceof Error) {
return { ok: false, error };
}
return {
ok: false,
error: new Error(`initializeSQL: Caught a non-error '${error}'`),
};
} finally {
sqlInitTimeEnd = Date.now();
}
// Only if we've initialized things successfully do we set up the corruption handler
runSQLCorruptionHandler();
2021-10-01 18:49:59 +00:00
return { ok: true, error: undefined };
}
2021-10-01 18:49:59 +00:00
const onDatabaseError = async (error: string) => {
// Prevent window from re-opening
ready = false;
if (mainWindow) {
settingsChannel?.invokeCallbackInMainWindow('closeDB', []);
mainWindow.close();
}
mainWindow = undefined;
2021-08-24 20:31:06 +00:00
const buttonIndex = dialog.showMessageBoxSync({
buttons: [
2021-10-01 18:49:59 +00:00
getLocale().i18n('deleteAndRestart'),
getLocale().i18n('copyErrorAndQuit'),
2021-08-24 20:31:06 +00:00
],
defaultId: 1,
cancelId: 1,
2021-08-24 20:31:06 +00:00
detail: redactAll(error),
2021-10-01 18:49:59 +00:00
message: getLocale().i18n('databaseError'),
2021-08-24 20:31:06 +00:00
noLink: true,
type: 'error',
});
if (buttonIndex === 1) {
2021-08-24 20:31:06 +00:00
clipboard.writeText(`Database startup error:\n\n${redactAll(error)}`);
} else {
await sql.removeDB();
userConfig.remove();
getLogger().error(
'onDatabaseError: Requesting immediate restart after quit'
);
2021-08-24 20:31:06 +00:00
app.relaunch();
}
getLogger().error('onDatabaseError: Quitting application');
2021-08-24 20:31:06 +00:00
app.exit(1);
};
2021-10-01 18:49:59 +00:00
let sqlInitPromise:
| Promise<{ ok: true; error: undefined } | { ok: false; error: Error }>
| undefined;
2021-10-01 18:49:59 +00:00
ipc.on('database-error', (_event: Electron.Event, error: string) => {
2021-08-24 20:31:06 +00:00
onDatabaseError(error);
});
function getAppLocale(): string {
return getEnvironment() === Environment.Test ? 'en' : app.getLocale();
}
2022-03-02 16:57:23 +00:00
// Signal doesn't really use media keys so we set this switch here to unblock
// them so that other apps can use them if they need to.
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling');
// If we don't set this, Desktop will ask for access to keychain/keyring on startup
app.commandLine.appendSwitch('password-store', 'basic');
2022-03-02 16:57:23 +00:00
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
let ready = false;
app.on('ready', async () => {
updateDefaultSession(session.defaultSession);
2022-01-20 01:50:16 +00:00
const [userDataPath, crashDumpsPath] = await Promise.all([
realpath(app.getPath('userData')),
realpath(app.getPath('crashDumps')),
]);
logger = await logging.initialize(getMainWindow);
2022-01-20 01:50:16 +00:00
await setupCrashReports(getLogger);
2022-01-11 20:02:46 +00:00
if (!locale) {
const appLocale = getAppLocale();
locale = loadLocale({ appLocale, logger });
}
sqlInitPromise = initializeSQL(userDataPath);
2021-01-15 17:30:58 +00:00
const startTime = Date.now();
2021-08-18 20:08:14 +00:00
settingsChannel = new SettingsChannel();
settingsChannel.install();
2021-01-15 17:30:58 +00:00
// We use this event only a single time to log the startup time of the app
// from when it's first ready until the loading screen disappears.
2021-04-13 23:43:56 +00:00
ipc.once('signal-app-loaded', (event, info) => {
const { preloadTime, connectTime, processedCount } = info;
2021-03-26 02:02:53 +00:00
const loadTime = Date.now() - startTime;
2021-04-06 17:29:22 +00:00
const sqlInitTime = sqlInitTimeEnd - sqlInitTimeStart;
2021-04-13 23:43:56 +00:00
const messageTime = loadTime - preloadTime - connectTime;
const messagesPerSec = (processedCount * 1000) / messageTime;
2021-10-01 18:49:59 +00:00
const innerLogger = getLogger();
innerLogger.info('App loaded - time:', loadTime);
innerLogger.info('SQL init - time:', sqlInitTime);
innerLogger.info('Preload - time:', preloadTime);
innerLogger.info('WebSocket connect - time:', connectTime);
innerLogger.info('Processed count:', processedCount);
innerLogger.info('Messages per second:', messagesPerSec);
2021-03-26 02:02:53 +00:00
2021-08-11 19:29:07 +00:00
event.sender.send('ci:event', 'app-loaded', {
loadTime,
sqlInitTime,
preloadTime,
connectTime,
processedCount,
messagesPerSec,
});
2021-01-15 17:30:58 +00:00
});
2022-01-11 20:02:46 +00:00
const installPath = await realpath(app.getAppPath());
2021-06-01 18:15:23 +00:00
addSensitivePath(userDataPath);
2022-01-20 01:50:16 +00:00
addSensitivePath(crashDumpsPath);
2021-06-01 18:15:23 +00:00
2021-12-09 08:06:04 +00:00
if (getEnvironment() !== Environment.Test) {
installFileHandler({
protocol: electronProtocol,
userDataPath,
installPath,
2021-02-01 20:01:25 +00:00
isWindows: OS.isWindows(),
});
}
installWebHandler({
2021-10-01 18:49:59 +00:00
enableHttp: Boolean(process.env.SIGNAL_ENABLE_HTTP),
protocol: electronProtocol,
});
logger.info('app ready');
logger.info(`starting version ${packageJson.version}`);
// This logging helps us debug user reports about broken devices.
{
let getMediaAccessStatus;
// This function is not supported on Linux, so we have a fallback.
if (systemPreferences.getMediaAccessStatus) {
2021-11-11 22:43:05 +00:00
getMediaAccessStatus =
systemPreferences.getMediaAccessStatus.bind(systemPreferences);
} else {
2021-10-01 18:49:59 +00:00
getMediaAccessStatus = noop;
}
logger.info(
'media access status',
getMediaAccessStatus('microphone'),
getMediaAccessStatus('camera')
);
}
GlobalErrors.updateLocale(locale.messages);
// If the sql initialization takes more than three seconds to complete, we
// want to notify the user that things are happening
const timeout = new Promise(resolveFn =>
setTimeout(resolveFn, 3000, 'timeout')
);
2022-05-11 22:58:14 +00:00
// This color is to be used only in loading screen and in this case we should
// never wait for the database to be initialized. Thus the theme setting
// lookup should be done only in ephemeral config.
const backgroundColor = await getBackgroundColor({ ephemeralOnly: true });
// eslint-disable-next-line more/no-then
Promise.race([sqlInitPromise, timeout]).then(async maybeTimeout => {
if (maybeTimeout !== 'timeout') {
return;
}
2021-10-01 18:49:59 +00:00
getLogger().info(
'sql.initialize is taking more than three seconds; showing loading dialog'
);
loadingWindow = new BrowserWindow({
show: false,
width: 300,
height: 265,
resizable: false,
frame: false,
2022-05-11 22:58:14 +00:00
backgroundColor,
webPreferences: {
...defaultWebPrefs,
nodeIntegration: false,
contextIsolation: true,
2021-10-01 18:49:59 +00:00
preload: join(__dirname, '../ts/windows/loading/preload.js'),
},
2020-04-20 21:07:16 +00:00
icon: windowIcon,
});
loadingWindow.once('ready-to-show', async () => {
2021-10-01 18:49:59 +00:00
if (!loadingWindow) {
return;
}
loadingWindow.show();
// Wait for sql initialization to complete, but ignore errors
await sqlInitPromise;
loadingWindow.destroy();
2021-10-01 18:49:59 +00:00
loadingWindow = undefined;
});
loadingWindow.loadURL(await prepareFileUrl([__dirname, '../loading.html']));
});
2021-06-01 18:06:38 +00:00
try {
await attachments.clearTempPath(userDataPath);
} catch (err) {
logger.error(
'main/ready: Error deleting temp dir:',
err && err.stack ? err.stack : err
);
}
// Initialize IPC channels before creating the window
attachmentChannel.initialize({
configDir: userDataPath,
cleanupOrphanedAttachments,
});
sqlChannels.initialize(sql);
2021-06-09 22:28:54 +00:00
PowerChannel.initialize({
send(event) {
if (!mainWindow) {
return;
}
mainWindow.webContents.send(event);
},
});
2021-06-01 18:06:38 +00:00
// Run window preloading in parallel with database initialization.
await createWindow();
const { error: sqlError } = await sqlInitPromise;
if (sqlError) {
2021-10-01 18:49:59 +00:00
getLogger().error('sql.initialize was unsuccessful; returning early');
2021-03-04 21:44:57 +00:00
2021-10-01 18:49:59 +00:00
await onDatabaseError(sqlError.stack || sqlError.message);
2021-03-04 21:44:57 +00:00
return;
}
2021-03-04 21:44:57 +00:00
2020-03-20 21:00:11 +00:00
appStartInitialSpellcheckSetting = await getSpellCheckSetting();
try {
const IDB_KEY = 'indexeddb-delete-needed';
const item = await sql.sqlCall('getItemById', [IDB_KEY]);
if (item && item.value) {
await sql.sqlCall('removeIndexedDBFiles', []);
await sql.sqlCall('removeItemById', [IDB_KEY]);
}
2021-03-04 21:44:57 +00:00
} catch (err) {
2021-10-01 18:49:59 +00:00
getLogger().error(
'(ready event handler) error deleting IndexedDB:',
2021-03-04 21:44:57 +00:00
err && err.stack ? err.stack : err
);
}
2018-08-08 17:00:33 +00:00
async function cleanupOrphanedAttachments() {
const allAttachments = await attachments.getAllAttachments(userDataPath);
const orphanedAttachments = await sql.sqlCall('removeKnownAttachments', [
allAttachments,
]);
2018-08-08 17:00:33 +00:00
await attachments.deleteAll({
userDataPath,
attachments: orphanedAttachments,
});
2021-11-02 23:01:13 +00:00
await attachments.deleteAllBadges({
userDataPath,
pathsToKeep: await sql.sqlCall('getAllBadgeImageFileLocalPaths', []),
});
const allStickers = await attachments.getAllStickers(userDataPath);
const orphanedStickers = await sql.sqlCall('removeKnownStickers', [
allStickers,
]);
await attachments.deleteAllStickers({
userDataPath,
stickers: orphanedStickers,
});
2019-08-07 00:40:25 +00:00
const allDraftAttachments = await attachments.getAllDraftAttachments(
userDataPath
);
const orphanedDraftAttachments = await sql.sqlCall(
'removeKnownDraftAttachments',
[allDraftAttachments]
2019-08-07 00:40:25 +00:00
);
await attachments.deleteAllDraftAttachments({
userDataPath,
attachments: orphanedDraftAttachments,
2019-08-07 00:40:25 +00:00
});
2018-08-08 17:00:33 +00:00
}
ready = true;
setupMenu();
systemTrayService = new SystemTrayService({ messages: locale.messages });
systemTrayService.setMainWindow(mainWindow);
systemTrayService.setEnabled(
shouldMinimizeToSystemTray(await systemTraySettingCache.get())
);
ensureFilePermissions([
'config.json',
'sql/db.sqlite',
'sql/db.sqlite-wal',
'sql/db.sqlite-shm',
]);
});
function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
UX Improvements: Global Menu & Copy Changes (#2078) - [x] Removed ‘Restart Signal’ global menu item - [x] Change _Click to create contact…_ to _Start conversation…_ - [x] Move global menu (top-left kebab) into OS menu bar, i.e. **Settings** > **Preferences…** - [x] Add tests for OS menu bar templates - [x] Fix bug with **Window** menu on macOS when showing setup options - [x] Use _Title Case_ for all OS menu bar menu items for consistency commit dedf7c9af0de90980388559659df0d92a77b864c Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 16:53:42 2018 -0500 Use ‘Title Case’ to be consistent with OS menus References: - Apple: - https://developer.apple.com/macos/human-interface-guidelines/menus/menu-anatomy/#menu-and-menu-item-titles - https://developer.apple.com/library/content/documentation/FinalCutProX/Conceptual/FxPlugHIG/TextStyleGuidelines/TextStyleGuidelines.html#//apple_ref/doc/uid/TP40013782-CH6-SW1 - https://titlecaseconverter.com/ commit 3286da29b334bd4526c587b17707c2f230cec8f5 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 16:36:50 2018 -0500 Fix bug for macOS ‘Window’ menu with setup options commit 236a23d1eafe2a16073394a27b9013298b682a25 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 16:27:46 2018 -0500 Test menus with included setup options commit c5d5f5abb8d7f52d6a4aa182a86c92f7ddceade0 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 16:10:27 2018 -0500 Move settings (‘Preferences’) into OS-level menu This reduces our reliance on custom UI until we have more design resources. commit 027803f8f4983cffa443f0beff1854dcf541689b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 16:02:56 2018 -0500 Prepare tests for menu with/without included setup commit 9e2f006924b85eb249a8a1261c1c4dd1a706afa6 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 15:55:46 2018 -0500 Destructure `includeSetup` commit 6b2a1eccdf724fd722e58415d2700da73942d9e8 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 15:55:14 2018 -0500 :abc: `createTemplate` `options` commit c2fecba34b153fed106f414ed3347d46299f6fe5 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 12:49:55 2018 -0500 Test menu for Windows and Linux commit 60281b1af9ad1f022cdbc40711ebd0b688a7355d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 12:40:39 2018 -0500 Add `yarn run test-app` command commit 1a0489919c0a97b03fe88196260fef894fb3d9e4 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 12:40:29 2018 -0500 Add test for `SignalMenu.createTemplate` on macOS commit 9638b86c0f00f231e44562a5aa01626f0e5fdd8b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 12:34:46 2018 -0500 Make `createTemplate` pure Extracting `options.platform` makes it easier to test without having to stub `process.platform`. commit 9c26404892d7c9a7bd0199a9e8367a165a3b365c Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 11:47:39 2018 -0500 Extract `locale.load` `appLocale` & `logger` for testability This allows us to run this code in a non-Electron environment, e.g. Node.js Mocha test suite. commit 710b22438df25c8d5e8431845a035c55ec8fc0b7 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 11:46:13 2018 -0500 :abc: npm scripts commit 9ae22937fbce078f91443023b560b3c0468c1380 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 11:45:30 2018 -0500 Use 2-space indendation for `app` module tests commit 22c26baf6159bd2e1f5a787c10e2260f09395329 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 11:22:55 2018 -0500 Prefer named exports commit 9c9526195266ac77ac2ca04135a1e675f617dfd2 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 11:22:46 2018 -0500 :abc: Organize `require`s commit 2f144d24d9e9a9ef72fe418996e3c911b304b00a Author: Daniel Gasienica <daniel@gasienica.ch> Date: Tue Feb 27 11:13:50 2018 -0500 Remove existing global hamburger menu This will be replaced by a OS-level ‘Preferences’ menu. commit f5adb374cb742e5f319ececda8ab6d8adee88d7e Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 26 18:40:54 2018 -0500 Remove ‘Restart Signal’ menu from settings Apparently, this is a remnant from the Chrome web application. commit d7a206bc8e67ef44022085e804ca040ed1b219f7 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 26 17:16:49 2018 -0500 Clarify label for starting a new conversation When user a enters a number that is not a contact, we prompt them to start a new conversation. commit 715a4064367fb61d85c1a4f9d48261b2ce002435 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Mon Feb 26 16:46:26 2018 -0500 Use ‘Enter name or number’ as prompt’ This follows implementation of Android and recommendation from Alissa.
2018-03-02 20:59:39 +00:00
const { platform } = process;
menuOptions = {
2021-10-01 18:49:59 +00:00
// options
New design for import/install, 'light' import (#2053) - A new design for the import flow. It features: - Icons at the top of every screen - Gray background, blue buttons, thinner text - Simpler copy - A new design for the install flow. It features: - Immediate entry into the QR code screen - Animated dots to show that we're loading the QR code from the server - Fewer screens: 1) QR 2) device name 3) sync-in-progress - When not set up, the app opens directly into the install screen, which has been streamlined. The `--import` command-line argument will cause the app to open directly into the import flow. - Support for two different flavors of builds - the normal build will open into the standard registration flow, and the import flavor will be exactly the same except during setup it will open directly into the import flow. - A new design for the (dev-only) standalone registration view - When these install sequences are active, the OS File menu has entries to allow you to switch the method of setup you'd like to use. These go away as soon as the first step is taken in any of these flows. - The device name (chosen on initial setup) is now shown in the settings panel - At the end of a light import, we hand off to the normal device link screen, starting at the QR code. On a full import, we remove the sensitive encryption information in the export to prevent conflicts on multiple imports. - `Whisper.Backup.exportToDirectory()` takes an options object so you can tell it to do a light export. - `Whisper.Backup.importFromDirectory()` takes an options object so you can force it to load only the light components found on disk. It also returns an object so you can tell whether a given import was a full import or light import. - On start of import, we build a list of all the ids present in the messages, conversations, and groups stores in IndexedDB. This can take some time if a lot of data is in the database already, but it makes the subsequent deduplicated import very fast. - Disappearing messages are now excluded when exporting - Remove some TODOs in the tests
2018-02-22 18:40:32 +00:00
development,
devTools: defaultWebPrefs.devTools,
2021-10-01 18:49:59 +00:00
includeSetup: false,
isProduction: isProduction(app.getVersion()),
platform,
// actions
forceUpdate,
2020-06-10 16:56:10 +00:00
openContactUs,
2021-10-01 18:49:59 +00:00
openForums,
2020-06-10 16:56:10 +00:00
openJoinTheBeta,
New design for import/install, 'light' import (#2053) - A new design for the import flow. It features: - Icons at the top of every screen - Gray background, blue buttons, thinner text - Simpler copy - A new design for the install flow. It features: - Immediate entry into the QR code screen - Animated dots to show that we're loading the QR code from the server - Fewer screens: 1) QR 2) device name 3) sync-in-progress - When not set up, the app opens directly into the install screen, which has been streamlined. The `--import` command-line argument will cause the app to open directly into the import flow. - Support for two different flavors of builds - the normal build will open into the standard registration flow, and the import flavor will be exactly the same except during setup it will open directly into the import flow. - A new design for the (dev-only) standalone registration view - When these install sequences are active, the OS File menu has entries to allow you to switch the method of setup you'd like to use. These go away as soon as the first step is taken in any of these flows. - The device name (chosen on initial setup) is now shown in the settings panel - At the end of a light import, we hand off to the normal device link screen, starting at the QR code. On a full import, we remove the sensitive encryption information in the export to prevent conflicts on multiple imports. - `Whisper.Backup.exportToDirectory()` takes an options object so you can tell it to do a light export. - `Whisper.Backup.importFromDirectory()` takes an options object so you can force it to load only the light components found on disk. It also returns an object so you can tell whether a given import was a full import or light import. - On start of import, we build a list of all the ids present in the messages, conversations, and groups stores in IndexedDB. This can take some time if a lot of data is in the database already, but it makes the subsequent deduplicated import very fast. - Disappearing messages are now excluded when exporting - Remove some TODOs in the tests
2018-02-22 18:40:32 +00:00
openReleaseNotes,
openSupportPage,
setupAsNewDevice,
setupAsStandalone,
2021-10-01 18:49:59 +00:00
showAbout,
showDebugLog: showDebugLogWindow,
showKeyboardShortcuts,
showSettings: showSettingsWindow,
showStickerCreator,
showWindow,
// overrides
...options,
2019-12-17 20:25:57 +00:00
};
2021-10-01 18:49:59 +00:00
const template = createTemplate(menuOptions, getLocale().messages);
New design for import/install, 'light' import (#2053) - A new design for the import flow. It features: - Icons at the top of every screen - Gray background, blue buttons, thinner text - Simpler copy - A new design for the install flow. It features: - Immediate entry into the QR code screen - Animated dots to show that we're loading the QR code from the server - Fewer screens: 1) QR 2) device name 3) sync-in-progress - When not set up, the app opens directly into the install screen, which has been streamlined. The `--import` command-line argument will cause the app to open directly into the import flow. - Support for two different flavors of builds - the normal build will open into the standard registration flow, and the import flavor will be exactly the same except during setup it will open directly into the import flow. - A new design for the (dev-only) standalone registration view - When these install sequences are active, the OS File menu has entries to allow you to switch the method of setup you'd like to use. These go away as soon as the first step is taken in any of these flows. - The device name (chosen on initial setup) is now shown in the settings panel - At the end of a light import, we hand off to the normal device link screen, starting at the QR code. On a full import, we remove the sensitive encryption information in the export to prevent conflicts on multiple imports. - `Whisper.Backup.exportToDirectory()` takes an options object so you can tell it to do a light export. - `Whisper.Backup.importFromDirectory()` takes an options object so you can force it to load only the light components found on disk. It also returns an object so you can tell whether a given import was a full import or light import. - On start of import, we build a list of all the ids present in the messages, conversations, and groups stores in IndexedDB. This can take some time if a lot of data is in the database already, but it makes the subsequent deduplicated import very fast. - Disappearing messages are now excluded when exporting - Remove some TODOs in the tests
2018-02-22 18:40:32 +00:00
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,
});
New design for import/install, 'light' import (#2053) - A new design for the import flow. It features: - Icons at the top of every screen - Gray background, blue buttons, thinner text - Simpler copy - A new design for the install flow. It features: - Immediate entry into the QR code screen - Animated dots to show that we're loading the QR code from the server - Fewer screens: 1) QR 2) device name 3) sync-in-progress - When not set up, the app opens directly into the install screen, which has been streamlined. The `--import` command-line argument will cause the app to open directly into the import flow. - Support for two different flavors of builds - the normal build will open into the standard registration flow, and the import flavor will be exactly the same except during setup it will open directly into the import flow. - A new design for the (dev-only) standalone registration view - When these install sequences are active, the OS File menu has entries to allow you to switch the method of setup you'd like to use. These go away as soon as the first step is taken in any of these flows. - The device name (chosen on initial setup) is now shown in the settings panel - At the end of a light import, we hand off to the normal device link screen, starting at the QR code. On a full import, we remove the sensitive encryption information in the export to prevent conflicts on multiple imports. - `Whisper.Backup.exportToDirectory()` takes an options object so you can tell it to do a light export. - `Whisper.Backup.importFromDirectory()` takes an options object so you can force it to load only the light components found on disk. It also returns an object so you can tell whether a given import was a full import or light import. - On start of import, we build a list of all the ids present in the messages, conversations, and groups stores in IndexedDB. This can take some time if a lot of data is in the database already, but it makes the subsequent deduplicated import very fast. - Disappearing messages are now excluded when exporting - Remove some TODOs in the tests
2018-02-22 18:40:32 +00:00
}
async function requestShutdown() {
if (!mainWindow || !mainWindow.webContents) {
return;
}
2021-10-01 18:49:59 +00:00
getLogger().info('requestShutdown: Requesting close of mainWindow...');
const request = new Promise<void>(resolveFn => {
2021-10-01 18:49:59 +00:00
let timeout: NodeJS.Timeout | undefined;
if (!mainWindow) {
resolveFn();
2021-10-01 18:49:59 +00:00
return;
}
ipc.once('now-ready-for-shutdown', (_event, error) => {
2021-10-01 18:49:59 +00:00
getLogger().info('requestShutdown: Response received');
if (error) {
getLogger().error(
'requestShutdown: got error, still shutting down.',
error
);
}
clearTimeoutIfNecessary(timeout);
resolveFn();
});
2021-10-01 18:49:59 +00:00
mainWindow.webContents.send('get-ready-for-shutdown');
// We'll wait two minutes, then force the app to go down. This can happen if someone
// exits the app before we've set everything up in preload() (so the browser isn't
// yet listening for these events), or if there are a whole lot of stacked-up tasks.
// Note: two minutes is also our timeout for SQL tasks in data.js in the browser.
2021-10-01 18:49:59 +00:00
timeout = setTimeout(() => {
getLogger().error(
'requestShutdown: Response never received; forcing shutdown.'
);
resolveFn();
}, 2 * 60 * 1000);
});
try {
await request;
} catch (error) {
2021-10-01 18:49:59 +00:00
getLogger().error(
'requestShutdown error:',
error && error.stack ? error.stack : error
);
}
}
app.on('before-quit', () => {
2021-10-01 18:49:59 +00:00
getLogger().info('before-quit event', {
readyForShutdown: windowState.readyForShutdown(),
shouldQuit: windowState.shouldQuit(),
});
systemTrayService?.markShouldQuit();
windowState.markShouldQuit();
2021-11-30 15:36:20 +00:00
if (mainWindow) {
mainWindow.webContents.send('quit');
}
});
// Quit when all windows are closed.
app.on('window-all-closed', () => {
2021-10-01 18:49:59 +00:00
getLogger().info('main process handling window-all-closed');
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
2021-10-01 18:49:59 +00:00
const shouldAutoClose = !OS.isMacOS() || isTestEnvironment(getEnvironment());
// Only automatically quit if the main window has been created
// This is necessary because `window-all-closed` can be triggered by the
// "optimizing application" window closing
if (shouldAutoClose && mainWindowCreated) {
app.quit();
}
});
app.on('activate', () => {
if (!ready) {
return;
}
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow) {
mainWindow.show();
} else {
createWindow();
}
});
// Defense in depth. We never intend to open webviews or windows. Prevent it completely.
2021-10-01 18:49:59 +00:00
app.on(
'web-contents-created',
(_createEvent: Electron.Event, contents: Electron.WebContents) => {
contents.on('will-attach-webview', attachEvent => {
attachEvent.preventDefault();
});
contents.on('new-window', newEvent => {
newEvent.preventDefault();
});
}
);
app.setAsDefaultProtocolClient('sgnl');
app.setAsDefaultProtocolClient('signalcaptcha');
app.on('will-finish-launching', () => {
// open-url must be set from within will-finish-launching for macOS
// https://stackoverflow.com/a/43949291
2020-08-27 19:44:51 +00:00
app.on('open-url', (event, incomingHref) => {
event.preventDefault();
2021-10-01 18:49:59 +00:00
if (isCaptchaHref(incomingHref, getLogger())) {
const { captcha } = parseCaptchaHref(incomingHref, getLogger());
challengeHandler.handleCaptcha(captcha);
2021-11-30 17:51:53 +00:00
// Show window after handling captcha
showWindow();
return;
}
2020-08-27 19:44:51 +00:00
handleSgnlHref(incomingHref);
});
});
2021-10-01 18:49:59 +00:00
ipc.on('set-badge-count', (_event: Electron.Event, count: number) => {
app.badgeCount = count;
});
New design for import/install, 'light' import (#2053) - A new design for the import flow. It features: - Icons at the top of every screen - Gray background, blue buttons, thinner text - Simpler copy - A new design for the install flow. It features: - Immediate entry into the QR code screen - Animated dots to show that we're loading the QR code from the server - Fewer screens: 1) QR 2) device name 3) sync-in-progress - When not set up, the app opens directly into the install screen, which has been streamlined. The `--import` command-line argument will cause the app to open directly into the import flow. - Support for two different flavors of builds - the normal build will open into the standard registration flow, and the import flavor will be exactly the same except during setup it will open directly into the import flow. - A new design for the (dev-only) standalone registration view - When these install sequences are active, the OS File menu has entries to allow you to switch the method of setup you'd like to use. These go away as soon as the first step is taken in any of these flows. - The device name (chosen on initial setup) is now shown in the settings panel - At the end of a light import, we hand off to the normal device link screen, starting at the QR code. On a full import, we remove the sensitive encryption information in the export to prevent conflicts on multiple imports. - `Whisper.Backup.exportToDirectory()` takes an options object so you can tell it to do a light export. - `Whisper.Backup.importFromDirectory()` takes an options object so you can force it to load only the light components found on disk. It also returns an object so you can tell whether a given import was a full import or light import. - On start of import, we build a list of all the ids present in the messages, conversations, and groups stores in IndexedDB. This can take some time if a lot of data is in the database already, but it makes the subsequent deduplicated import very fast. - Disappearing messages are now excluded when exporting - Remove some TODOs in the tests
2018-02-22 18:40:32 +00:00
ipc.on('remove-setup-menu-items', () => {
setupMenu();
});
ipc.on('add-setup-menu-items', () => {
setupMenu({
includeSetup: true,
});
});
ipc.on('draw-attention', () => {
if (!mainWindow) {
return;
}
2021-02-01 20:01:25 +00:00
if (OS.isWindows() || OS.isLinux()) {
mainWindow.flashFrame(true);
}
});
ipc.on('restart', () => {
2021-10-01 18:49:59 +00:00
getLogger().info('Relaunching application');
app.relaunch();
app.quit();
});
2020-04-28 21:18:41 +00:00
ipc.on('shutdown', () => {
app.quit();
});
2021-10-01 18:49:59 +00:00
ipc.on(
'set-auto-hide-menu-bar',
(_event: Electron.Event, autoHide: boolean) => {
if (mainWindow) {
mainWindow.autoHideMenuBar = autoHide;
}
}
2021-10-01 18:49:59 +00:00
);
2021-10-01 18:49:59 +00:00
ipc.on(
'set-menu-bar-visibility',
(_event: Electron.Event, visibility: boolean) => {
if (mainWindow) {
mainWindow.setMenuBarVisibility(visibility);
}
}
2021-10-01 18:49:59 +00:00
);
2021-11-11 22:43:05 +00:00
ipc.on(
'update-system-tray-setting',
(_event, rawSystemTraySetting /* : Readonly<unknown> */) => {
const systemTraySetting = parseSystemTraySetting(rawSystemTraySetting);
systemTraySettingCache.set(systemTraySetting);
2021-11-11 22:43:05 +00:00
if (systemTrayService) {
const isEnabled = shouldMinimizeToSystemTray(systemTraySetting);
systemTrayService.setEnabled(isEnabled);
}
}
2021-11-11 22:43:05 +00:00
);
ipc.on('close-screen-share-controller', () => {
if (screenShareWindow) {
screenShareWindow.close();
}
});
ipc.on('stop-screen-share', () => {
if (mainWindow) {
mainWindow.webContents.send('stop-screen-share');
}
});
2021-10-01 18:49:59 +00:00
ipc.on('show-screen-share', (_event: Electron.Event, sourceName: string) => {
showScreenShareWindow(sourceName);
});
2021-10-01 18:49:59 +00:00
ipc.on('update-tray-icon', (_event: Electron.Event, unreadCount: number) => {
if (systemTrayService) {
systemTrayService.setUnreadCount(unreadCount);
}
});
// Debug Log-related IPC calls
ipc.on('show-debug-log', showDebugLogWindow);
ipc.on(
'show-debug-log-save-dialog',
async (_event: Electron.Event, logText: string) => {
const { filePath } = await dialog.showSaveDialog({
defaultPath: 'debuglog.txt',
});
if (filePath) {
await writeFile(filePath, logText);
}
}
);
// Permissions Popup-related IPC calls
2021-10-01 18:49:59 +00:00
ipc.handle(
2021-11-05 08:47:32 +00:00
'show-permissions-popup',
async (_event: Electron.Event, forCalling: boolean, forCamera: boolean) => {
2021-10-01 18:49:59 +00:00
try {
2021-11-05 08:47:32 +00:00
await showPermissionsPopupWindow(forCalling, forCamera);
2021-10-01 18:49:59 +00:00
} catch (error) {
getLogger().error(
2021-11-05 08:47:32 +00:00
'show-permissions-popup error:',
2021-10-01 18:49:59 +00:00
error && error.stack ? error.stack : error
);
}
2020-06-04 18:16:19 +00:00
}
2021-10-01 18:49:59 +00:00
);
// Settings-related IPC calls
function addDarkOverlay() {
if (mainWindow && mainWindow.webContents) {
mainWindow.webContents.send('add-dark-overlay');
}
}
function removeDarkOverlay() {
if (mainWindow && mainWindow.webContents) {
mainWindow.webContents.send('remove-dark-overlay');
}
}
ipc.on('show-settings', showSettingsWindow);
ipc.on('delete-all-data', () => {
2021-08-24 20:57:34 +00:00
if (settingsWindow) {
settingsWindow.close();
}
if (mainWindow && mainWindow.webContents) {
mainWindow.webContents.send('delete-all-data');
}
});
2019-10-04 18:06:17 +00:00
ipc.on('get-built-in-images', async () => {
2021-10-01 18:49:59 +00:00
if (!mainWindow) {
getLogger().warn('ipc/get-built-in-images: No mainWindow!');
return;
}
2019-10-04 18:06:17 +00:00
try {
const images = await attachments.getBuiltInImages();
mainWindow.webContents.send('get-success-built-in-images', null, images);
} catch (error) {
if (mainWindow && mainWindow.webContents) {
mainWindow.webContents.send('get-success-built-in-images', error.message);
} else {
2021-10-01 18:49:59 +00:00
getLogger().error('Error handling get-built-in-images:', error.stack);
2019-10-04 18:06:17 +00:00
}
}
});
// Ingested in preload.js via a sendSync call
ipc.on('locale-data', event => {
// eslint-disable-next-line no-param-reassign
2021-10-01 18:49:59 +00:00
event.returnValue = getLocale().messages;
});
2021-03-05 22:27:07 +00:00
ipc.on('user-config-key', event => {
2021-03-04 21:44:57 +00:00
// eslint-disable-next-line no-param-reassign
event.returnValue = userConfig.get('key');
});
ipc.on('get-user-data-path', event => {
// eslint-disable-next-line no-param-reassign
event.returnValue = app.getPath('userData');
});
2021-08-18 20:08:14 +00:00
// Refresh the settings window whenever preferences change
ipc.on('preferences-changed', () => {
2021-09-29 18:37:30 +00:00
for (const window of activeWindows) {
if (window.webContents) {
window.webContents.send('preferences-changed');
}
2021-08-18 20:08:14 +00:00
}
});
2021-10-01 18:49:59 +00:00
function getIncomingHref(argv: Array<string>) {
return argv.find(arg => isSgnlHref(arg, getLogger()));
}
2021-10-01 18:49:59 +00:00
function getIncomingCaptchaHref(argv: Array<string>) {
return argv.find(arg => isCaptchaHref(arg, getLogger()));
}
2021-10-01 18:49:59 +00:00
function handleSgnlHref(incomingHref: string) {
let command;
let args;
let hash;
2021-10-01 18:49:59 +00:00
if (isSgnlHref(incomingHref, getLogger())) {
({ command, args, hash } = parseSgnlHref(incomingHref, getLogger()));
} else if (isSignalHttpsLink(incomingHref, getLogger())) {
({ command, args, hash } = parseSignalHttpsLink(incomingHref, getLogger()));
}
2021-08-28 13:27:38 +00:00
if (mainWindow && mainWindow.webContents) {
if (command === 'addstickers') {
2021-10-01 18:49:59 +00:00
getLogger().info('Opening sticker pack from sgnl protocol link');
const packId = args?.get('pack_id');
const packKeyHex = args?.get('pack_key');
2021-08-28 13:27:38 +00:00
const packKey = packKeyHex
? Buffer.from(packKeyHex, 'hex').toString('base64')
: '';
mainWindow.webContents.send('show-sticker-pack', { packId, packKey });
} else if (command === 'signal.group' && hash) {
2021-10-01 18:49:59 +00:00
getLogger().info('Showing group from sgnl protocol link');
2021-08-28 13:27:38 +00:00
mainWindow.webContents.send('show-group-via-link', { hash });
} else if (command === 'signal.me' && hash) {
2021-10-01 18:49:59 +00:00
getLogger().info('Showing conversation from sgnl protocol link');
2021-08-28 13:27:38 +00:00
mainWindow.webContents.send('show-conversation-via-signal.me', { hash });
} else {
2021-10-01 18:49:59 +00:00
getLogger().info('Showing warning that we cannot process link');
2021-08-28 13:27:38 +00:00
mainWindow.webContents.send('unknown-sgnl-link');
}
} else {
2021-10-01 18:49:59 +00:00
getLogger().error('Unhandled sgnl link');
}
}
2019-12-17 20:25:57 +00:00
ipc.on('install-sticker-pack', (_event, packId, packKeyHex) => {
const packKey = Buffer.from(packKeyHex, 'hex').toString('base64');
2021-10-01 18:49:59 +00:00
if (mainWindow) {
mainWindow.webContents.send('install-sticker-pack', { packId, packKey });
}
2019-12-17 20:25:57 +00:00
});
ipc.on('ensure-file-permissions', async event => {
await ensureFilePermissions();
event.reply('ensure-file-permissions-done');
});
/**
* Ensure files in the user's data directory have the proper permissions.
* Optionally takes an array of file paths to exclusively affect.
*
* @param {string[]} [onlyFiles] - Only ensure permissions on these given files
*/
2021-10-01 18:49:59 +00:00
async function ensureFilePermissions(onlyFiles?: Array<string>) {
getLogger().info('Begin ensuring permissions');
const start = Date.now();
2022-01-11 20:02:46 +00:00
const userDataPath = await realpath(app.getPath('userData'));
// fast-glob uses `/` for all platforms
2021-10-01 18:49:59 +00:00
const userDataGlob = normalizePath(join(userDataPath, '**', '*'));
// Determine files to touch
const files = onlyFiles
2021-10-01 18:49:59 +00:00
? onlyFiles.map(f => join(userDataPath, f))
: await fastGlob(userDataGlob, {
markDirectories: true,
onlyFiles: false,
ignore: ['**/Singleton*'],
});
2021-10-01 18:49:59 +00:00
getLogger().info(`Ensuring file permissions for ${files.length} files`);
// Touch each file in a queue
const q = new PQueue({ concurrency: 5, timeout: 1000 * 60 * 2 });
q.addAll(
files.map(f => async () => {
const isDir = f.endsWith('/');
try {
2021-10-01 18:49:59 +00:00
await chmod(normalize(f), isDir ? 0o700 : 0o600);
} catch (error) {
2021-10-01 18:49:59 +00:00
getLogger().error(
'ensureFilePermissions: Error from chmod',
error.message
);
}
})
);
await q.onEmpty();
2021-10-01 18:49:59 +00:00
getLogger().info(`Finish ensuring permissions in ${Date.now() - start}ms`);
}
2021-10-27 17:54:16 +00:00
ipc.handle('get-auto-launch', async () => {
return app.getLoginItemSettings().openAtLogin;
});
ipc.handle('set-auto-launch', async (_event, value) => {
app.setLoginItemSettings({ openAtLogin: Boolean(value) });
});
ipc.on('show-message-box', (_event, { type, message }) => {
dialog.showMessageBox({ type, message });
});
ipc.on('show-item-in-folder', (_event, folder) => {
shell.showItemInFolder(folder);
});
ipc.handle('show-save-dialog', async (_event, { defaultPath }) => {
if (!mainWindow) {
getLogger().warn('show-save-dialog: no main window');
return { canceled: true };
}
return dialog.showSaveDialog(mainWindow, {
defaultPath,
});
});
2021-12-09 08:06:04 +00:00
ipc.handle('getScreenCaptureSources', async () => {
return desktopCapturer.getSources({
fetchWindowIcons: true,
thumbnailSize: { height: 102, width: 184 },
types: ['window', 'screen'],
});
});
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);
}
});
2021-12-09 08:06:04 +00:00
if (isTestEnvironment(getEnvironment())) {
ipc.handle('ci:test-electron:done', async (_event, info) => {
2021-12-17 23:12:42 +00:00
if (!process.env.TEST_QUIT_ON_COMPLETE) {
return;
}
2021-12-09 08:06:04 +00:00
process.stdout.write(
`ci:test-electron:done=${JSON.stringify(info)}\n`,
() => app.quit()
);
});
}