Move all files under /app to typescript
This commit is contained in:
parent
7bb6ad534f
commit
24960d481e
40 changed files with 745 additions and 620 deletions
|
@ -24,6 +24,7 @@ libtextsecure/test/blanket_mocha.js
|
||||||
test/blanket_mocha.js
|
test/blanket_mocha.js
|
||||||
|
|
||||||
# TypeScript generated files
|
# TypeScript generated files
|
||||||
|
app/**/*.js
|
||||||
ts/**/*.js
|
ts/**/*.js
|
||||||
sticker-creator/**/*.js
|
sticker-creator/**/*.js
|
||||||
!sticker-creator/preload.js
|
!sticker-creator/preload.js
|
||||||
|
|
|
@ -143,7 +143,7 @@ module.exports = {
|
||||||
|
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['ts/**/*.ts', 'ts/**/*.tsx'],
|
files: ['ts/**/*.ts', 'ts/**/*.tsx', 'app/**/*.ts'],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: 'tsconfig.json',
|
project: 'tsconfig.json',
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -26,6 +26,7 @@ stylesheets/*.css
|
||||||
test/test.js
|
test/test.js
|
||||||
|
|
||||||
# React / TypeScript
|
# React / TypeScript
|
||||||
|
app/*.js
|
||||||
ts/**/*.js
|
ts/**/*.js
|
||||||
ts/protobuf/*.d.ts
|
ts/protobuf/*.d.ts
|
||||||
sticker-creator/**/*.js
|
sticker-creator/**/*.js
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# supports `.gitignore`: https://github.com/prettier/prettier/issues/2294
|
# supports `.gitignore`: https://github.com/prettier/prettier/issues/2294
|
||||||
|
|
||||||
# Generated files
|
# Generated files
|
||||||
|
app/**/*.js
|
||||||
config/local-*.json
|
config/local-*.json
|
||||||
config/local.json
|
config/local.json
|
||||||
dist/**
|
dist/**
|
||||||
|
|
|
@ -3120,33 +3120,6 @@ Signal Desktop makes use of the following open source projects.
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
## to-arraybuffer
|
|
||||||
|
|
||||||
The MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2016 John Hiesey
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge,
|
|
||||||
to any person obtaining a copy of this software and
|
|
||||||
associated documentation files (the "Software"), to
|
|
||||||
deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify,
|
|
||||||
merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom
|
|
||||||
the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice
|
|
||||||
shall be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
|
||||||
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
## typeface-inter
|
## typeface-inter
|
||||||
|
|
||||||
Copyright (c) 2016-2018 The Inter Project Authors (me@rsms.me)
|
Copyright (c) 2016-2018 The Inter Project Authors (me@rsms.me)
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
module.exports = {
|
// For reference: https://github.com/airbnb/javascript
|
||||||
rules: {
|
|
||||||
// On the node.js side, we're still using console.log
|
const rules = {
|
||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
},
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
rules,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const electron = require('electron');
|
import { ipcMain } from 'electron';
|
||||||
const rimraf = require('rimraf');
|
import * as rimraf from 'rimraf';
|
||||||
const Attachments = require('./attachments');
|
import {
|
||||||
|
getPath,
|
||||||
const { ipcMain } = electron;
|
getStickersPath,
|
||||||
|
getTempPath,
|
||||||
module.exports = {
|
getDraftPath,
|
||||||
initialize,
|
} from './attachments';
|
||||||
};
|
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
|
@ -19,16 +18,22 @@ const ERASE_TEMP_KEY = 'erase-temp';
|
||||||
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
||||||
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
||||||
|
|
||||||
function initialize({ configDir, cleanupOrphanedAttachments }) {
|
export function initialize({
|
||||||
|
configDir,
|
||||||
|
cleanupOrphanedAttachments,
|
||||||
|
}: {
|
||||||
|
configDir: string;
|
||||||
|
cleanupOrphanedAttachments: () => Promise<void>;
|
||||||
|
}): void {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
throw new Error('initialze: Already initialized!');
|
throw new Error('initialze: Already initialized!');
|
||||||
}
|
}
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
const attachmentsDir = Attachments.getPath(configDir);
|
const attachmentsDir = getPath(configDir);
|
||||||
const stickersDir = Attachments.getStickersPath(configDir);
|
const stickersDir = getStickersPath(configDir);
|
||||||
const tempDir = Attachments.getTempPath(configDir);
|
const tempDir = getTempPath(configDir);
|
||||||
const draftDir = Attachments.getDraftPath(configDir);
|
const draftDir = getDraftPath(configDir);
|
||||||
|
|
||||||
ipcMain.on(ERASE_TEMP_KEY, event => {
|
ipcMain.on(ERASE_TEMP_KEY, event => {
|
||||||
try {
|
try {
|
4
app/attachments.d.ts
vendored
4
app/attachments.d.ts
vendored
|
@ -1,4 +0,0 @@
|
||||||
// Copyright 2019-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
export function getTempPath(userDataPath: string): string;
|
|
|
@ -1,26 +1,30 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const crypto = require('crypto');
|
import { randomBytes } from 'crypto';
|
||||||
const path = require('path');
|
import { basename, extname, join, normalize, relative } from 'path';
|
||||||
const { app, dialog, shell, remote } = require('electron');
|
import { app, dialog, shell, remote } from 'electron';
|
||||||
|
|
||||||
const fastGlob = require('fast-glob');
|
import fastGlob from 'fast-glob';
|
||||||
const glob = require('glob');
|
import glob from 'glob';
|
||||||
const pify = require('pify');
|
import pify from 'pify';
|
||||||
const fse = require('fs-extra');
|
import fse from 'fs-extra';
|
||||||
const toArrayBuffer = require('to-arraybuffer');
|
import { map, isArrayBuffer, isString } from 'lodash';
|
||||||
const { map, isArrayBuffer, isString } = require('lodash');
|
import normalizePath from 'normalize-path';
|
||||||
const normalizePath = require('normalize-path');
|
import sanitizeFilename from 'sanitize-filename';
|
||||||
const sanitizeFilename = require('sanitize-filename');
|
import getGuid from 'uuid/v4';
|
||||||
const getGuid = require('uuid/v4');
|
|
||||||
const { isPathInside } = require('../ts/util/isPathInside');
|
import { typedArrayToArrayBuffer } from '../ts/Crypto';
|
||||||
const { isWindows } = require('../ts/OS');
|
import { isPathInside } from '../ts/util/isPathInside';
|
||||||
const {
|
import { isWindows } from '../ts/OS';
|
||||||
writeWindowsZoneIdentifier,
|
import { writeWindowsZoneIdentifier } from '../ts/util/windowsZoneIdentifier';
|
||||||
} = require('../ts/util/windowsZoneIdentifier');
|
|
||||||
|
type FSAttrType = {
|
||||||
|
set: (path: string, attribute: string, value: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let xattr: FSAttrType | undefined;
|
||||||
|
|
||||||
let xattr;
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
// eslint-disable-next-line global-require, import/no-extraneous-dependencies, import/no-unresolved
|
// eslint-disable-next-line global-require, import/no-extraneous-dependencies, import/no-unresolved
|
||||||
|
@ -36,113 +40,115 @@ const DRAFT_PATH = 'drafts.noindex';
|
||||||
|
|
||||||
const getApp = () => app || remote.app;
|
const getApp = () => app || remote.app;
|
||||||
|
|
||||||
exports.getAllAttachments = async userDataPath => {
|
export const getAllAttachments = async (
|
||||||
const dir = exports.getPath(userDataPath);
|
userDataPath: string
|
||||||
const pattern = normalizePath(path.join(dir, '**', '*'));
|
): Promise<ReadonlyArray<string>> => {
|
||||||
|
const dir = getPath(userDataPath);
|
||||||
|
const pattern = normalizePath(join(dir, '**', '*'));
|
||||||
|
|
||||||
const files = await fastGlob(pattern, { onlyFiles: true });
|
const files = await fastGlob(pattern, { onlyFiles: true });
|
||||||
return map(files, file => path.relative(dir, file));
|
return map(files, file => relative(dir, file));
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getAllStickers = async userDataPath => {
|
export const getAllStickers = async (
|
||||||
const dir = exports.getStickersPath(userDataPath);
|
userDataPath: string
|
||||||
const pattern = normalizePath(path.join(dir, '**', '*'));
|
): Promise<ReadonlyArray<string>> => {
|
||||||
|
const dir = getStickersPath(userDataPath);
|
||||||
|
const pattern = normalizePath(join(dir, '**', '*'));
|
||||||
|
|
||||||
const files = await fastGlob(pattern, { onlyFiles: true });
|
const files = await fastGlob(pattern, { onlyFiles: true });
|
||||||
return map(files, file => path.relative(dir, file));
|
return map(files, file => relative(dir, file));
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getAllDraftAttachments = async userDataPath => {
|
export const getAllDraftAttachments = async (
|
||||||
const dir = exports.getDraftPath(userDataPath);
|
userDataPath: string
|
||||||
const pattern = normalizePath(path.join(dir, '**', '*'));
|
): Promise<ReadonlyArray<string>> => {
|
||||||
|
const dir = getDraftPath(userDataPath);
|
||||||
|
const pattern = normalizePath(join(dir, '**', '*'));
|
||||||
|
|
||||||
const files = await fastGlob(pattern, { onlyFiles: true });
|
const files = await fastGlob(pattern, { onlyFiles: true });
|
||||||
return map(files, file => path.relative(dir, file));
|
return map(files, file => relative(dir, file));
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getBuiltInImages = async () => {
|
export const getBuiltInImages = async (): Promise<ReadonlyArray<string>> => {
|
||||||
const dir = path.join(__dirname, '../images');
|
const dir = join(__dirname, '../images');
|
||||||
const pattern = path.join(dir, '**', '*.svg');
|
const pattern = join(dir, '**', '*.svg');
|
||||||
|
|
||||||
// Note: we cannot use fast-glob here because, inside of .asar files, readdir will not
|
// Note: we cannot use fast-glob here because, inside of .asar files, readdir will not
|
||||||
// honor the withFileTypes flag: https://github.com/electron/electron/issues/19074
|
// honor the withFileTypes flag: https://github.com/electron/electron/issues/19074
|
||||||
const files = await pify(glob)(pattern, { nodir: true });
|
const files = await pify(glob)(pattern, { nodir: true });
|
||||||
return map(files, file => path.relative(dir, file));
|
return map(files, file => relative(dir, file));
|
||||||
};
|
};
|
||||||
|
|
||||||
// getPath :: AbsolutePath -> AbsolutePath
|
export const getPath = (userDataPath: string): string => {
|
||||||
exports.getPath = userDataPath => {
|
|
||||||
if (!isString(userDataPath)) {
|
if (!isString(userDataPath)) {
|
||||||
throw new TypeError("'userDataPath' must be a string");
|
throw new TypeError("'userDataPath' must be a string");
|
||||||
}
|
}
|
||||||
return path.join(userDataPath, PATH);
|
return join(userDataPath, PATH);
|
||||||
};
|
};
|
||||||
|
|
||||||
// getStickersPath :: AbsolutePath -> AbsolutePath
|
export const getStickersPath = (userDataPath: string): string => {
|
||||||
exports.getStickersPath = userDataPath => {
|
|
||||||
if (!isString(userDataPath)) {
|
if (!isString(userDataPath)) {
|
||||||
throw new TypeError("'userDataPath' must be a string");
|
throw new TypeError("'userDataPath' must be a string");
|
||||||
}
|
}
|
||||||
return path.join(userDataPath, STICKER_PATH);
|
return join(userDataPath, STICKER_PATH);
|
||||||
};
|
};
|
||||||
|
|
||||||
// getTempPath :: AbsolutePath -> AbsolutePath
|
export const getTempPath = (userDataPath: string): string => {
|
||||||
exports.getTempPath = userDataPath => {
|
|
||||||
if (!isString(userDataPath)) {
|
if (!isString(userDataPath)) {
|
||||||
throw new TypeError("'userDataPath' must be a string");
|
throw new TypeError("'userDataPath' must be a string");
|
||||||
}
|
}
|
||||||
return path.join(userDataPath, TEMP_PATH);
|
return join(userDataPath, TEMP_PATH);
|
||||||
};
|
};
|
||||||
|
|
||||||
// getDraftPath :: AbsolutePath -> AbsolutePath
|
export const getDraftPath = (userDataPath: string): string => {
|
||||||
exports.getDraftPath = userDataPath => {
|
|
||||||
if (!isString(userDataPath)) {
|
if (!isString(userDataPath)) {
|
||||||
throw new TypeError("'userDataPath' must be a string");
|
throw new TypeError("'userDataPath' must be a string");
|
||||||
}
|
}
|
||||||
return path.join(userDataPath, DRAFT_PATH);
|
return join(userDataPath, DRAFT_PATH);
|
||||||
};
|
};
|
||||||
|
|
||||||
// clearTempPath :: AbsolutePath -> AbsolutePath
|
export const clearTempPath = (userDataPath: string): Promise<void> => {
|
||||||
exports.clearTempPath = userDataPath => {
|
const tempPath = getTempPath(userDataPath);
|
||||||
const tempPath = exports.getTempPath(userDataPath);
|
|
||||||
return fse.emptyDir(tempPath);
|
return fse.emptyDir(tempPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
// createReader :: AttachmentsPath ->
|
export const createReader = (
|
||||||
// RelativePath ->
|
root: string
|
||||||
// IO (Promise ArrayBuffer)
|
): ((relativePath: string) => Promise<ArrayBuffer>) => {
|
||||||
exports.createReader = root => {
|
|
||||||
if (!isString(root)) {
|
if (!isString(root)) {
|
||||||
throw new TypeError("'root' must be a path");
|
throw new TypeError("'root' must be a path");
|
||||||
}
|
}
|
||||||
|
|
||||||
return async relativePath => {
|
return async (relativePath: string): Promise<ArrayBuffer> => {
|
||||||
if (!isString(relativePath)) {
|
if (!isString(relativePath)) {
|
||||||
throw new TypeError("'relativePath' must be a string");
|
throw new TypeError("'relativePath' must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const absolutePath = path.join(root, relativePath);
|
const absolutePath = join(root, relativePath);
|
||||||
const normalized = path.normalize(absolutePath);
|
const normalized = normalize(absolutePath);
|
||||||
if (!isPathInside(normalized, root)) {
|
if (!isPathInside(normalized, root)) {
|
||||||
throw new Error('Invalid relative path');
|
throw new Error('Invalid relative path');
|
||||||
}
|
}
|
||||||
const buffer = await fse.readFile(normalized);
|
const buffer = await fse.readFile(normalized);
|
||||||
return toArrayBuffer(buffer);
|
return typedArrayToArrayBuffer(buffer);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.createDoesExist = root => {
|
export const createDoesExist = (
|
||||||
|
root: string
|
||||||
|
): ((relativePath: string) => Promise<boolean>) => {
|
||||||
if (!isString(root)) {
|
if (!isString(root)) {
|
||||||
throw new TypeError("'root' must be a path");
|
throw new TypeError("'root' must be a path");
|
||||||
}
|
}
|
||||||
|
|
||||||
return async relativePath => {
|
return async (relativePath: string): Promise<boolean> => {
|
||||||
if (!isString(relativePath)) {
|
if (!isString(relativePath)) {
|
||||||
throw new TypeError("'relativePath' must be a string");
|
throw new TypeError("'relativePath' must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const absolutePath = path.join(root, relativePath);
|
const absolutePath = join(root, relativePath);
|
||||||
const normalized = path.normalize(absolutePath);
|
const normalized = normalize(absolutePath);
|
||||||
if (!isPathInside(normalized, root)) {
|
if (!isPathInside(normalized, root)) {
|
||||||
throw new Error('Invalid relative path');
|
throw new Error('Invalid relative path');
|
||||||
}
|
}
|
||||||
|
@ -155,14 +161,16 @@ exports.createDoesExist = root => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.copyIntoAttachmentsDirectory = root => {
|
export const copyIntoAttachmentsDirectory = (
|
||||||
|
root: string
|
||||||
|
): ((sourcePath: string) => Promise<string>) => {
|
||||||
if (!isString(root)) {
|
if (!isString(root)) {
|
||||||
throw new TypeError("'root' must be a path");
|
throw new TypeError("'root' must be a path");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userDataPath = getApp().getPath('userData');
|
const userDataPath = getApp().getPath('userData');
|
||||||
|
|
||||||
return async sourcePath => {
|
return async (sourcePath: string): Promise<string> => {
|
||||||
if (!isString(sourcePath)) {
|
if (!isString(sourcePath)) {
|
||||||
throw new TypeError('sourcePath must be a string');
|
throw new TypeError('sourcePath must be a string');
|
||||||
}
|
}
|
||||||
|
@ -173,10 +181,10 @@ exports.copyIntoAttachmentsDirectory = root => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = exports.createName();
|
const name = createName();
|
||||||
const relativePath = exports.getRelativePath(name);
|
const relativePath = getRelativePath(name);
|
||||||
const absolutePath = path.join(root, relativePath);
|
const absolutePath = join(root, relativePath);
|
||||||
const normalized = path.normalize(absolutePath);
|
const normalized = normalize(absolutePath);
|
||||||
if (!isPathInside(normalized, root)) {
|
if (!isPathInside(normalized, root)) {
|
||||||
throw new Error('Invalid relative path');
|
throw new Error('Invalid relative path');
|
||||||
}
|
}
|
||||||
|
@ -187,15 +195,22 @@ exports.copyIntoAttachmentsDirectory = root => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.writeToDownloads = async ({ data, name }) => {
|
export const writeToDownloads = async ({
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
data: ArrayBuffer;
|
||||||
|
name: string;
|
||||||
|
}): Promise<{ fullPath: string; name: string }> => {
|
||||||
const appToUse = getApp();
|
const appToUse = getApp();
|
||||||
const downloadsPath =
|
const downloadsPath =
|
||||||
appToUse.getPath('downloads') || appToUse.getPath('home');
|
appToUse.getPath('downloads') || appToUse.getPath('home');
|
||||||
const sanitized = sanitizeFilename(name);
|
const sanitized = sanitizeFilename(name);
|
||||||
|
|
||||||
const extension = path.extname(sanitized);
|
const extension = extname(sanitized);
|
||||||
const basename = path.basename(sanitized, extension);
|
const fileBasename = basename(sanitized, extension);
|
||||||
const getCandidateName = count => `${basename} (${count})${extension}`;
|
const getCandidateName = (count: number) =>
|
||||||
|
`${fileBasename} (${count})${extension}`;
|
||||||
|
|
||||||
const existingFiles = await fse.readdir(downloadsPath);
|
const existingFiles = await fse.readdir(downloadsPath);
|
||||||
let candidateName = sanitized;
|
let candidateName = sanitized;
|
||||||
|
@ -205,13 +220,13 @@ exports.writeToDownloads = async ({ data, name }) => {
|
||||||
candidateName = getCandidateName(count);
|
candidateName = getCandidateName(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = path.join(downloadsPath, candidateName);
|
const target = join(downloadsPath, candidateName);
|
||||||
const normalized = path.normalize(target);
|
const normalized = normalize(target);
|
||||||
if (!isPathInside(normalized, downloadsPath)) {
|
if (!isPathInside(normalized, downloadsPath)) {
|
||||||
throw new Error('Invalid filename!');
|
throw new Error('Invalid filename!');
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeWithAttributes(normalized, Buffer.from(data));
|
await writeWithAttributes(normalized, data);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fullPath: normalized,
|
fullPath: normalized,
|
||||||
|
@ -219,7 +234,10 @@ exports.writeToDownloads = async ({ data, name }) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function writeWithAttributes(target, data) {
|
async function writeWithAttributes(
|
||||||
|
target: string,
|
||||||
|
data: ArrayBuffer
|
||||||
|
): Promise<void> {
|
||||||
await fse.writeFile(target, Buffer.from(data));
|
await fse.writeFile(target, Buffer.from(data));
|
||||||
|
|
||||||
if (process.platform === 'darwin' && xattr) {
|
if (process.platform === 'darwin' && xattr) {
|
||||||
|
@ -246,15 +264,15 @@ async function writeWithAttributes(target, data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.openFileInDownloads = async name => {
|
export const openFileInDownloads = async (name: string): Promise<void> => {
|
||||||
const shellToUse = shell || remote.shell;
|
const shellToUse = shell || remote.shell;
|
||||||
const appToUse = getApp();
|
const appToUse = getApp();
|
||||||
|
|
||||||
const downloadsPath =
|
const downloadsPath =
|
||||||
appToUse.getPath('downloads') || appToUse.getPath('home');
|
appToUse.getPath('downloads') || appToUse.getPath('home');
|
||||||
const target = path.join(downloadsPath, name);
|
const target = join(downloadsPath, name);
|
||||||
|
|
||||||
const normalized = path.normalize(target);
|
const normalized = normalize(target);
|
||||||
if (!isPathInside(normalized, downloadsPath)) {
|
if (!isPathInside(normalized, downloadsPath)) {
|
||||||
throw new Error('Invalid filename!');
|
throw new Error('Invalid filename!');
|
||||||
}
|
}
|
||||||
|
@ -262,7 +280,13 @@ exports.openFileInDownloads = async name => {
|
||||||
shellToUse.showItemInFolder(normalized);
|
shellToUse.showItemInFolder(normalized);
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.saveAttachmentToDisk = async ({ data, name }) => {
|
export const saveAttachmentToDisk = async ({
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
data: ArrayBuffer;
|
||||||
|
name: string;
|
||||||
|
}): Promise<null | { fullPath: string; name: string }> => {
|
||||||
const dialogToUse = dialog || remote.dialog;
|
const dialogToUse = dialog || remote.dialog;
|
||||||
const browserWindow = remote.getCurrentWindow();
|
const browserWindow = remote.getCurrentWindow();
|
||||||
|
|
||||||
|
@ -273,57 +297,61 @@ exports.saveAttachmentToDisk = async ({ data, name }) => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (canceled) {
|
if (canceled || !filePath) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeWithAttributes(filePath, Buffer.from(data));
|
await writeWithAttributes(filePath, data);
|
||||||
|
|
||||||
const basename = path.basename(filePath);
|
const fileBasename = basename(filePath);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fullPath: filePath,
|
fullPath: filePath,
|
||||||
name: basename,
|
name: fileBasename,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.openFileInFolder = async target => {
|
export const openFileInFolder = async (target: string): Promise<void> => {
|
||||||
const shellToUse = shell || remote.shell;
|
const shellToUse = shell || remote.shell;
|
||||||
|
|
||||||
shellToUse.showItemInFolder(target);
|
shellToUse.showItemInFolder(target);
|
||||||
};
|
};
|
||||||
|
|
||||||
// createWriterForNew :: AttachmentsPath ->
|
export const createWriterForNew = (
|
||||||
// ArrayBuffer ->
|
root: string
|
||||||
// IO (Promise RelativePath)
|
): ((arrayBuffer: ArrayBuffer) => Promise<string>) => {
|
||||||
exports.createWriterForNew = root => {
|
|
||||||
if (!isString(root)) {
|
if (!isString(root)) {
|
||||||
throw new TypeError("'root' must be a path");
|
throw new TypeError("'root' must be a path");
|
||||||
}
|
}
|
||||||
|
|
||||||
return async arrayBuffer => {
|
return async (arrayBuffer: ArrayBuffer) => {
|
||||||
if (!isArrayBuffer(arrayBuffer)) {
|
if (!isArrayBuffer(arrayBuffer)) {
|
||||||
throw new TypeError("'arrayBuffer' must be an array buffer");
|
throw new TypeError("'arrayBuffer' must be an array buffer");
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = exports.createName();
|
const name = createName();
|
||||||
const relativePath = exports.getRelativePath(name);
|
const relativePath = getRelativePath(name);
|
||||||
return exports.createWriterForExisting(root)({
|
return createWriterForExisting(root)({
|
||||||
data: arrayBuffer,
|
data: arrayBuffer,
|
||||||
path: relativePath,
|
path: relativePath,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// createWriter :: AttachmentsPath ->
|
export const createWriterForExisting = (
|
||||||
// { data: ArrayBuffer, path: RelativePath } ->
|
root: string
|
||||||
// IO (Promise RelativePath)
|
): ((options: { data: ArrayBuffer; path: string }) => Promise<string>) => {
|
||||||
exports.createWriterForExisting = root => {
|
|
||||||
if (!isString(root)) {
|
if (!isString(root)) {
|
||||||
throw new TypeError("'root' must be a path");
|
throw new TypeError("'root' must be a path");
|
||||||
}
|
}
|
||||||
|
|
||||||
return async ({ data: arrayBuffer, path: relativePath } = {}) => {
|
return async ({
|
||||||
|
data: arrayBuffer,
|
||||||
|
path: relativePath,
|
||||||
|
}: {
|
||||||
|
data: ArrayBuffer;
|
||||||
|
path: string;
|
||||||
|
}): Promise<string> => {
|
||||||
if (!isString(relativePath)) {
|
if (!isString(relativePath)) {
|
||||||
throw new TypeError("'relativePath' must be a path");
|
throw new TypeError("'relativePath' must be a path");
|
||||||
}
|
}
|
||||||
|
@ -333,8 +361,8 @@ exports.createWriterForExisting = root => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
const absolutePath = path.join(root, relativePath);
|
const absolutePath = join(root, relativePath);
|
||||||
const normalized = path.normalize(absolutePath);
|
const normalized = normalize(absolutePath);
|
||||||
if (!isPathInside(normalized, root)) {
|
if (!isPathInside(normalized, root)) {
|
||||||
throw new Error('Invalid relative path');
|
throw new Error('Invalid relative path');
|
||||||
}
|
}
|
||||||
|
@ -345,21 +373,20 @@ exports.createWriterForExisting = root => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// createDeleter :: AttachmentsPath ->
|
export const createDeleter = (
|
||||||
// RelativePath ->
|
root: string
|
||||||
// IO Unit
|
): ((relativePath: string) => Promise<void>) => {
|
||||||
exports.createDeleter = root => {
|
|
||||||
if (!isString(root)) {
|
if (!isString(root)) {
|
||||||
throw new TypeError("'root' must be a path");
|
throw new TypeError("'root' must be a path");
|
||||||
}
|
}
|
||||||
|
|
||||||
return async relativePath => {
|
return async (relativePath: string): Promise<void> => {
|
||||||
if (!isString(relativePath)) {
|
if (!isString(relativePath)) {
|
||||||
throw new TypeError("'relativePath' must be a string");
|
throw new TypeError("'relativePath' must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const absolutePath = path.join(root, relativePath);
|
const absolutePath = join(root, relativePath);
|
||||||
const normalized = path.normalize(absolutePath);
|
const normalized = normalize(absolutePath);
|
||||||
if (!isPathInside(normalized, root)) {
|
if (!isPathInside(normalized, root)) {
|
||||||
throw new Error('Invalid relative path');
|
throw new Error('Invalid relative path');
|
||||||
}
|
}
|
||||||
|
@ -367,8 +394,14 @@ exports.createDeleter = root => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.deleteAll = async ({ userDataPath, attachments }) => {
|
export const deleteAll = async ({
|
||||||
const deleteFromDisk = exports.createDeleter(exports.getPath(userDataPath));
|
userDataPath,
|
||||||
|
attachments,
|
||||||
|
}: {
|
||||||
|
userDataPath: string;
|
||||||
|
attachments: ReadonlyArray<string>;
|
||||||
|
}): Promise<void> => {
|
||||||
|
const deleteFromDisk = createDeleter(getPath(userDataPath));
|
||||||
|
|
||||||
for (let index = 0, max = attachments.length; index < max; index += 1) {
|
for (let index = 0, max = attachments.length; index < max; index += 1) {
|
||||||
const file = attachments[index];
|
const file = attachments[index];
|
||||||
|
@ -379,10 +412,14 @@ exports.deleteAll = async ({ userDataPath, attachments }) => {
|
||||||
console.log(`deleteAll: deleted ${attachments.length} files`);
|
console.log(`deleteAll: deleted ${attachments.length} files`);
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.deleteAllStickers = async ({ userDataPath, stickers }) => {
|
export const deleteAllStickers = async ({
|
||||||
const deleteFromDisk = exports.createDeleter(
|
userDataPath,
|
||||||
exports.getStickersPath(userDataPath)
|
stickers,
|
||||||
);
|
}: {
|
||||||
|
userDataPath: string;
|
||||||
|
stickers: ReadonlyArray<string>;
|
||||||
|
}): Promise<void> => {
|
||||||
|
const deleteFromDisk = createDeleter(getStickersPath(userDataPath));
|
||||||
|
|
||||||
for (let index = 0, max = stickers.length; index < max; index += 1) {
|
for (let index = 0, max = stickers.length; index < max; index += 1) {
|
||||||
const file = stickers[index];
|
const file = stickers[index];
|
||||||
|
@ -393,40 +430,43 @@ exports.deleteAllStickers = async ({ userDataPath, stickers }) => {
|
||||||
console.log(`deleteAllStickers: deleted ${stickers.length} files`);
|
console.log(`deleteAllStickers: deleted ${stickers.length} files`);
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.deleteAllDraftAttachments = async ({ userDataPath, stickers }) => {
|
export const deleteAllDraftAttachments = async ({
|
||||||
const deleteFromDisk = exports.createDeleter(
|
userDataPath,
|
||||||
exports.getDraftPath(userDataPath)
|
attachments,
|
||||||
);
|
}: {
|
||||||
|
userDataPath: string;
|
||||||
|
attachments: ReadonlyArray<string>;
|
||||||
|
}): Promise<void> => {
|
||||||
|
const deleteFromDisk = createDeleter(getDraftPath(userDataPath));
|
||||||
|
|
||||||
for (let index = 0, max = stickers.length; index < max; index += 1) {
|
for (let index = 0, max = attachments.length; index < max; index += 1) {
|
||||||
const file = stickers[index];
|
const file = attachments[index];
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await deleteFromDisk(file);
|
await deleteFromDisk(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`deleteAllDraftAttachments: deleted ${stickers.length} files`);
|
console.log(`deleteAllDraftAttachments: deleted ${attachments.length} files`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// createName :: Unit -> IO String
|
export const createName = (): string => {
|
||||||
exports.createName = () => {
|
const buffer = randomBytes(32);
|
||||||
const buffer = crypto.randomBytes(32);
|
|
||||||
return buffer.toString('hex');
|
return buffer.toString('hex');
|
||||||
};
|
};
|
||||||
|
|
||||||
// getRelativePath :: String -> Path
|
export const getRelativePath = (name: string): string => {
|
||||||
exports.getRelativePath = name => {
|
|
||||||
if (!isString(name)) {
|
if (!isString(name)) {
|
||||||
throw new TypeError("'name' must be a string");
|
throw new TypeError("'name' must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix = name.slice(0, 2);
|
const prefix = name.slice(0, 2);
|
||||||
return path.join(prefix, name);
|
return join(prefix, name);
|
||||||
};
|
};
|
||||||
|
|
||||||
// createAbsolutePathGetter :: RootPath -> RelativePath -> AbsolutePath
|
export const createAbsolutePathGetter = (rootPath: string) => (
|
||||||
exports.createAbsolutePathGetter = rootPath => relativePath => {
|
relativePath: string
|
||||||
const absolutePath = path.join(rootPath, relativePath);
|
): string => {
|
||||||
const normalized = path.normalize(absolutePath);
|
const absolutePath = join(rootPath, relativePath);
|
||||||
|
const normalized = normalize(absolutePath);
|
||||||
if (!isPathInside(normalized, rootPath)) {
|
if (!isPathInside(normalized, rootPath)) {
|
||||||
throw new Error('Invalid relative path');
|
throw new Error('Invalid relative path');
|
||||||
}
|
}
|
|
@ -1,62 +0,0 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const ENCODING = 'utf8';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
start,
|
|
||||||
};
|
|
||||||
|
|
||||||
function start(name, targetPath, options = {}) {
|
|
||||||
const { allowMalformedOnStartup } = options;
|
|
||||||
let cachedValue = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const text = fs.readFileSync(targetPath, ENCODING);
|
|
||||||
cachedValue = JSON.parse(text);
|
|
||||||
console.log(`config/get: Successfully read ${name} config file`);
|
|
||||||
|
|
||||||
if (!cachedValue) {
|
|
||||||
console.log(
|
|
||||||
`config/get: ${name} config value was falsy, cache is now empty object`
|
|
||||||
);
|
|
||||||
cachedValue = Object.create(null);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (!allowMalformedOnStartup && error.code !== 'ENOENT') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`config/get: Did not find ${name} config file, cache is now empty object`
|
|
||||||
);
|
|
||||||
cachedValue = Object.create(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get(keyPath) {
|
|
||||||
return _.get(cachedValue, keyPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
function set(keyPath, value) {
|
|
||||||
_.set(cachedValue, keyPath, value);
|
|
||||||
console.log(`config/set: Saving ${name} config to disk`);
|
|
||||||
const text = JSON.stringify(cachedValue, null, ' ');
|
|
||||||
fs.writeFileSync(targetPath, text, ENCODING);
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove() {
|
|
||||||
console.log(`config/remove: Deleting ${name} config from disk`);
|
|
||||||
fs.unlinkSync(targetPath);
|
|
||||||
cachedValue = Object.create(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
set,
|
|
||||||
get,
|
|
||||||
remove,
|
|
||||||
};
|
|
||||||
}
|
|
71
app/base_config.ts
Normal file
71
app/base_config.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright 2018-2020 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
|
||||||
|
|
||||||
|
import { get, set } from 'lodash';
|
||||||
|
|
||||||
|
const ENCODING = 'utf8';
|
||||||
|
|
||||||
|
type ConfigType = Record<string, unknown>;
|
||||||
|
|
||||||
|
export function start(
|
||||||
|
name: string,
|
||||||
|
targetPath: string,
|
||||||
|
options?: { allowMalformedOnStartup?: boolean }
|
||||||
|
): {
|
||||||
|
set: (keyPath: string, value: unknown) => void;
|
||||||
|
get: (keyPath: string) => unknown;
|
||||||
|
remove: () => void;
|
||||||
|
} {
|
||||||
|
let cachedValue: ConfigType | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = readFileSync(targetPath, ENCODING);
|
||||||
|
cachedValue = JSON.parse(text);
|
||||||
|
console.log(`config/get: Successfully read ${name} config file`);
|
||||||
|
|
||||||
|
if (!cachedValue) {
|
||||||
|
console.log(
|
||||||
|
`config/get: ${name} config value was falsy, cache is now empty object`
|
||||||
|
);
|
||||||
|
cachedValue = Object.create(null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!options?.allowMalformedOnStartup && error.code !== 'ENOENT') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`config/get: Did not find ${name} config file, cache is now empty object`
|
||||||
|
);
|
||||||
|
cachedValue = Object.create(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ourGet(keyPath: string): unknown {
|
||||||
|
return get(cachedValue, keyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ourSet(keyPath: string, value: unknown): void {
|
||||||
|
if (!cachedValue) {
|
||||||
|
throw new Error('ourSet: no cachedValue!');
|
||||||
|
}
|
||||||
|
|
||||||
|
set(cachedValue, keyPath, value);
|
||||||
|
console.log(`config/set: Saving ${name} config to disk`);
|
||||||
|
const text = JSON.stringify(cachedValue, null, ' ');
|
||||||
|
writeFileSync(targetPath, text, ENCODING);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(): void {
|
||||||
|
console.log(`config/remove: Deleting ${name} config from disk`);
|
||||||
|
unlinkSync(targetPath);
|
||||||
|
cachedValue = Object.create(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
set: ourSet,
|
||||||
|
get: ourGet,
|
||||||
|
remove,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,14 +1,15 @@
|
||||||
// Copyright 2017-2020 Signal Messenger, LLC
|
// Copyright 2017-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const path = require('path');
|
import { join } from 'path';
|
||||||
const { app } = require('electron');
|
import { app } from 'electron';
|
||||||
const {
|
|
||||||
|
import {
|
||||||
Environment,
|
Environment,
|
||||||
getEnvironment,
|
getEnvironment,
|
||||||
setEnvironment,
|
setEnvironment,
|
||||||
parseEnvironment,
|
parseEnvironment,
|
||||||
} = require('../ts/environment');
|
} from '../ts/environment';
|
||||||
|
|
||||||
// In production mode, NODE_ENV cannot be customized by the user
|
// In production mode, NODE_ENV cannot be customized by the user
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
|
@ -19,12 +20,12 @@ if (app.isPackaged) {
|
||||||
|
|
||||||
// Set environment vars to configure node-config before requiring it
|
// Set environment vars to configure node-config before requiring it
|
||||||
process.env.NODE_ENV = getEnvironment();
|
process.env.NODE_ENV = getEnvironment();
|
||||||
process.env.NODE_CONFIG_DIR = path.join(__dirname, '..', 'config');
|
process.env.NODE_CONFIG_DIR = join(__dirname, '..', 'config');
|
||||||
|
|
||||||
if (getEnvironment() === Environment.Production) {
|
if (getEnvironment() === Environment.Production) {
|
||||||
// harden production config against the local env
|
// harden production config against the local env
|
||||||
process.env.NODE_CONFIG = '';
|
process.env.NODE_CONFIG = '';
|
||||||
process.env.NODE_CONFIG_STRICT_MODE = true;
|
process.env.NODE_CONFIG_STRICT_MODE = 'true';
|
||||||
process.env.HOSTNAME = '';
|
process.env.HOSTNAME = '';
|
||||||
process.env.NODE_APP_INSTANCE = '';
|
process.env.NODE_APP_INSTANCE = '';
|
||||||
process.env.ALLOW_CONFIG_MUTATIONS = '';
|
process.env.ALLOW_CONFIG_MUTATIONS = '';
|
||||||
|
@ -33,9 +34,18 @@ if (getEnvironment() === Environment.Production) {
|
||||||
process.env.SIGNAL_ENABLE_HTTP = '';
|
process.env.SIGNAL_ENABLE_HTTP = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConfigType = {
|
||||||
|
get: (key: string) => unknown;
|
||||||
|
has: (key: string) => unknown;
|
||||||
|
[key: string]: unknown;
|
||||||
|
util: {
|
||||||
|
getEnv: (keY: string) => string | undefined;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// We load config after we've made our modifications to NODE_ENV
|
// We load config after we've made our modifications to NODE_ENV
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const config = require('config');
|
const config: ConfigType = require('config');
|
||||||
|
|
||||||
config.environment = getEnvironment();
|
config.environment = getEnvironment();
|
||||||
config.enableHttp = process.env.SIGNAL_ENABLE_HTTP;
|
config.enableHttp = process.env.SIGNAL_ENABLE_HTTP;
|
||||||
|
@ -54,4 +64,4 @@ config.enableHttp = process.env.SIGNAL_ENABLE_HTTP;
|
||||||
console.log(`${s} ${config.util.getEnv(s)}`);
|
console.log(`${s} ${config.util.getEnv(s)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = config;
|
export default config;
|
|
@ -1,17 +0,0 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const { app } = require('electron');
|
|
||||||
|
|
||||||
const { start } = require('./base_config');
|
|
||||||
|
|
||||||
const userDataPath = app.getPath('userData');
|
|
||||||
const targetPath = path.join(userDataPath, 'ephemeral.json');
|
|
||||||
|
|
||||||
const ephemeralConfig = start('ephemeral', targetPath, {
|
|
||||||
allowMalformedOnStartup: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = ephemeralConfig;
|
|
19
app/ephemeral_config.ts
Normal file
19
app/ephemeral_config.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { app } from 'electron';
|
||||||
|
|
||||||
|
import { start } from './base_config';
|
||||||
|
|
||||||
|
const userDataPath = app.getPath('userData');
|
||||||
|
const targetPath = join(userDataPath, 'ephemeral.json');
|
||||||
|
|
||||||
|
const ephemeralConfig = start('ephemeral', targetPath, {
|
||||||
|
allowMalformedOnStartup: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const get = ephemeralConfig.get.bind(ephemeralConfig);
|
||||||
|
export const remove = ephemeralConfig.remove.bind(ephemeralConfig);
|
||||||
|
export const set = ephemeralConfig.set.bind(ephemeralConfig);
|
|
@ -1,55 +0,0 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
const electron = require('electron');
|
|
||||||
|
|
||||||
const Errors = require('../js/modules/types/errors');
|
|
||||||
|
|
||||||
const { app, dialog, clipboard } = electron;
|
|
||||||
const { redactAll } = require('../ts/util/privacy');
|
|
||||||
|
|
||||||
// We use hard-coded strings until we're able to update these strings from the locale.
|
|
||||||
let quitText = 'Quit';
|
|
||||||
let copyErrorAndQuitText = 'Copy error and quit';
|
|
||||||
|
|
||||||
function handleError(prefix, error) {
|
|
||||||
if (console._error) {
|
|
||||||
console._error(`${prefix}:`, Errors.toLogFormat(error));
|
|
||||||
}
|
|
||||||
console.error(`${prefix}:`, Errors.toLogFormat(error));
|
|
||||||
|
|
||||||
if (app.isReady()) {
|
|
||||||
// title field is not shown on macOS, so we don't use it
|
|
||||||
const buttonIndex = dialog.showMessageBoxSync({
|
|
||||||
buttons: [quitText, copyErrorAndQuitText],
|
|
||||||
defaultId: 0,
|
|
||||||
detail: redactAll(error.stack),
|
|
||||||
message: prefix,
|
|
||||||
noLink: true,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (buttonIndex === 1) {
|
|
||||||
clipboard.writeText(`${prefix}\n\n${redactAll(error.stack)}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dialog.showErrorBox(prefix, error.stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updateLocale = messages => {
|
|
||||||
quitText = messages.quit.message;
|
|
||||||
copyErrorAndQuitText = messages.copyErrorAndQuit.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.addHandler = () => {
|
|
||||||
process.on('uncaughtException', error => {
|
|
||||||
handleError('Unhandled Error', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('unhandledRejection', error => {
|
|
||||||
handleError('Unhandled Promise Rejection', error);
|
|
||||||
});
|
|
||||||
};
|
|
64
app/global_errors.ts
Normal file
64
app/global_errors.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2018-2020 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { app, dialog, clipboard } from 'electron';
|
||||||
|
|
||||||
|
import * as Errors from '../js/modules/types/errors';
|
||||||
|
import { redactAll } from '../ts/util/privacy';
|
||||||
|
import { LocaleMessagesType } from '../ts/types/I18N';
|
||||||
|
import { reallyJsonStringify } from '../ts/util/reallyJsonStringify';
|
||||||
|
|
||||||
|
// We use hard-coded strings until we're able to update these strings from the locale.
|
||||||
|
let quitText = 'Quit';
|
||||||
|
let copyErrorAndQuitText = 'Copy error and quit';
|
||||||
|
|
||||||
|
function handleError(prefix: string, error: Error): void {
|
||||||
|
if (console._error) {
|
||||||
|
console._error(`${prefix}:`, Errors.toLogFormat(error));
|
||||||
|
}
|
||||||
|
console.error(`${prefix}:`, Errors.toLogFormat(error));
|
||||||
|
|
||||||
|
if (app.isReady()) {
|
||||||
|
// title field is not shown on macOS, so we don't use it
|
||||||
|
const buttonIndex = dialog.showMessageBoxSync({
|
||||||
|
buttons: [quitText, copyErrorAndQuitText],
|
||||||
|
defaultId: 0,
|
||||||
|
detail: redactAll(error.stack || ''),
|
||||||
|
message: prefix,
|
||||||
|
noLink: true,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (buttonIndex === 1) {
|
||||||
|
clipboard.writeText(`${prefix}\n\n${redactAll(error.stack || '')}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialog.showErrorBox(prefix, error.stack || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
app.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateLocale = (messages: LocaleMessagesType): void => {
|
||||||
|
quitText = messages.quit.message;
|
||||||
|
copyErrorAndQuitText = messages.copyErrorAndQuit.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getError(reason: unknown): Error {
|
||||||
|
if (reason instanceof Error) {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorString = reallyJsonStringify(reason);
|
||||||
|
return new Error(`Promise rejected with a non-error: ${errorString}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addHandler = (): void => {
|
||||||
|
process.on('uncaughtException', (reason: unknown) => {
|
||||||
|
handleError('Unhandled Error', _getError(reason));
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason: unknown) => {
|
||||||
|
handleError('Unhandled Promise Rejection', _getError(reason));
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,12 +1,15 @@
|
||||||
// Copyright 2017-2020 Signal Messenger, LLC
|
// Copyright 2017-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const path = require('path');
|
import { join } from 'path';
|
||||||
const fs = require('fs');
|
import { readFileSync } from 'fs';
|
||||||
const _ = require('lodash');
|
import { merge } from 'lodash';
|
||||||
const { setup } = require('../js/modules/i18n');
|
import { setup } from '../js/modules/i18n';
|
||||||
|
|
||||||
function normalizeLocaleName(locale) {
|
import { LoggerType } from '../ts/types/Logging';
|
||||||
|
import { LocalizerType, LocaleMessagesType } from '../ts/types/I18N';
|
||||||
|
|
||||||
|
function normalizeLocaleName(locale: string): string {
|
||||||
if (/^en-/.test(locale)) {
|
if (/^en-/.test(locale)) {
|
||||||
return 'en';
|
return 'en';
|
||||||
}
|
}
|
||||||
|
@ -14,10 +17,10 @@ function normalizeLocaleName(locale) {
|
||||||
return locale;
|
return locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLocaleMessages(locale) {
|
function getLocaleMessages(locale: string): LocaleMessagesType {
|
||||||
const onDiskLocale = locale.replace('-', '_');
|
const onDiskLocale = locale.replace('-', '_');
|
||||||
|
|
||||||
const targetFile = path.join(
|
const targetFile = join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'..',
|
||||||
'_locales',
|
'_locales',
|
||||||
|
@ -25,10 +28,20 @@ function getLocaleMessages(locale) {
|
||||||
'messages.json'
|
'messages.json'
|
||||||
);
|
);
|
||||||
|
|
||||||
return JSON.parse(fs.readFileSync(targetFile, 'utf-8'));
|
return JSON.parse(readFileSync(targetFile, 'utf-8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function load({ appLocale, logger } = {}) {
|
export function load({
|
||||||
|
appLocale,
|
||||||
|
logger,
|
||||||
|
}: {
|
||||||
|
appLocale: string;
|
||||||
|
logger: LoggerType;
|
||||||
|
}): {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
name: string;
|
||||||
|
messages: LocaleMessagesType;
|
||||||
|
} {
|
||||||
if (!appLocale) {
|
if (!appLocale) {
|
||||||
throw new TypeError('`appLocale` is required');
|
throw new TypeError('`appLocale` is required');
|
||||||
}
|
}
|
||||||
|
@ -51,7 +64,7 @@ function load({ appLocale, logger } = {}) {
|
||||||
messages = getLocaleMessages(localeName);
|
messages = getLocaleMessages(localeName);
|
||||||
|
|
||||||
// We start with english, then overwrite that with anything present in locale
|
// We start with english, then overwrite that with anything present in locale
|
||||||
messages = _.merge(english, messages);
|
messages = merge(english, messages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Problem loading messages for locale ${localeName} ${e.stack}`
|
`Problem loading messages for locale ${localeName} ${e.stack}`
|
||||||
|
@ -70,7 +83,3 @@ function load({ appLocale, logger } = {}) {
|
||||||
messages,
|
messages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
load,
|
|
||||||
};
|
|
|
@ -1,9 +1,41 @@
|
||||||
// Copyright 2017-2020 Signal Messenger, LLC
|
// Copyright 2017-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const { isString } = require('lodash');
|
import { isString } from 'lodash';
|
||||||
|
import { MenuItemConstructorOptions } from 'electron';
|
||||||
|
|
||||||
exports.createTemplate = (options, messages) => {
|
import { LocaleMessagesType } from '../ts/types/I18N';
|
||||||
|
|
||||||
|
export type MenuListType = Array<MenuItemConstructorOptions>;
|
||||||
|
|
||||||
|
type OptionsType = {
|
||||||
|
// options
|
||||||
|
development: boolean;
|
||||||
|
devTools: boolean;
|
||||||
|
includeSetup: boolean;
|
||||||
|
isBeta: (version: string) => boolean;
|
||||||
|
platform: string;
|
||||||
|
|
||||||
|
// actions
|
||||||
|
openContactUs: () => unknown;
|
||||||
|
openForums: () => unknown;
|
||||||
|
openJoinTheBeta: () => unknown;
|
||||||
|
openReleaseNotes: () => unknown;
|
||||||
|
openSupportPage: () => unknown;
|
||||||
|
setupAsNewDevice: () => unknown;
|
||||||
|
setupAsStandalone: () => unknown;
|
||||||
|
showAbout: () => unknown;
|
||||||
|
showDebugLog: () => unknown;
|
||||||
|
showKeyboardShortcuts: () => unknown;
|
||||||
|
showSettings: () => unknown;
|
||||||
|
showStickerCreator: () => unknown;
|
||||||
|
showWindow: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTemplate = (
|
||||||
|
options: OptionsType,
|
||||||
|
messages: LocaleMessagesType
|
||||||
|
): MenuListType => {
|
||||||
if (!isString(options.platform)) {
|
if (!isString(options.platform)) {
|
||||||
throw new TypeError('`options.platform` must be a string');
|
throw new TypeError('`options.platform` must be a string');
|
||||||
}
|
}
|
||||||
|
@ -27,7 +59,7 @@ exports.createTemplate = (options, messages) => {
|
||||||
showStickerCreator,
|
showStickerCreator,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const template = [
|
const template: MenuListType = [
|
||||||
{
|
{
|
||||||
label: messages.mainMenuFile.message,
|
label: messages.mainMenuFile.message,
|
||||||
submenu: [
|
submenu: [
|
||||||
|
@ -76,7 +108,7 @@ exports.createTemplate = (options, messages) => {
|
||||||
label: messages.editMenuPaste.message,
|
label: messages.editMenuPaste.message,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'pasteandmatchstyle',
|
role: 'pasteAndMatchStyle',
|
||||||
label: messages.editMenuPasteAndMatchStyle.message,
|
label: messages.editMenuPasteAndMatchStyle.message,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -84,7 +116,7 @@ exports.createTemplate = (options, messages) => {
|
||||||
label: messages.editMenuDelete.message,
|
label: messages.editMenuDelete.message,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'selectall',
|
role: 'selectAll',
|
||||||
label: messages.editMenuSelectAll.message,
|
label: messages.editMenuSelectAll.message,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -93,16 +125,16 @@ exports.createTemplate = (options, messages) => {
|
||||||
label: messages.mainMenuView.message,
|
label: messages.mainMenuView.message,
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
role: 'resetzoom',
|
role: 'resetZoom',
|
||||||
label: messages.viewMenuResetZoom.message,
|
label: messages.viewMenuResetZoom.message,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accelerator: platform === 'darwin' ? 'Command+=' : 'Control+=',
|
accelerator: platform === 'darwin' ? 'Command+=' : 'Control+=',
|
||||||
role: 'zoomin',
|
role: 'zoomIn',
|
||||||
label: messages.viewMenuZoomIn.message,
|
label: messages.viewMenuZoomIn.message,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'zoomout',
|
role: 'zoomOut',
|
||||||
label: messages.viewMenuZoomOut.message,
|
label: messages.viewMenuZoomOut.message,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -122,10 +154,10 @@ exports.createTemplate = (options, messages) => {
|
||||||
...(devTools
|
...(devTools
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
type: 'separator',
|
type: 'separator' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'toggledevtools',
|
role: 'toggleDevTools' as const,
|
||||||
label: messages.viewMenuToggleDevTools.message,
|
label: messages.viewMenuToggleDevTools.message,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -192,21 +224,25 @@ exports.createTemplate = (options, messages) => {
|
||||||
if (includeSetup) {
|
if (includeSetup) {
|
||||||
const fileMenu = template[0];
|
const fileMenu = template[0];
|
||||||
|
|
||||||
// These are in reverse order, since we're prepending them one at a time
|
if (Array.isArray(fileMenu.submenu)) {
|
||||||
if (options.development) {
|
// These are in reverse order, since we're prepending them one at a time
|
||||||
fileMenu.submenu.unshift({
|
if (options.development) {
|
||||||
label: messages.menuSetupAsStandalone.message,
|
fileMenu.submenu.unshift({
|
||||||
click: setupAsStandalone,
|
label: messages.menuSetupAsStandalone.message,
|
||||||
});
|
click: setupAsStandalone,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fileMenu.submenu.unshift({
|
fileMenu.submenu.unshift({
|
||||||
type: 'separator',
|
type: 'separator',
|
||||||
});
|
});
|
||||||
fileMenu.submenu.unshift({
|
fileMenu.submenu.unshift({
|
||||||
label: messages.menuSetupAsNewDevice.message,
|
label: messages.menuSetupAsNewDevice.message,
|
||||||
click: setupAsNewDevice,
|
click: setupAsNewDevice,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('createTemplate: fileMenu.submenu was not an array!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
|
@ -216,30 +252,43 @@ exports.createTemplate = (options, messages) => {
|
||||||
return template;
|
return template;
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateForMac(template, messages, options) {
|
function updateForMac(
|
||||||
|
template: MenuListType,
|
||||||
|
messages: LocaleMessagesType,
|
||||||
|
options: OptionsType
|
||||||
|
): MenuListType {
|
||||||
const { showAbout, showSettings, showWindow } = options;
|
const { showAbout, showSettings, showWindow } = options;
|
||||||
|
|
||||||
// Remove About item and separator from Help menu, since they're in the app menu
|
// Remove About item and separator from Help menu, since they're in the app menu
|
||||||
template[4].submenu.pop();
|
const aboutMenu = template[4];
|
||||||
template[4].submenu.pop();
|
if (Array.isArray(aboutMenu.submenu)) {
|
||||||
|
aboutMenu.submenu.pop();
|
||||||
|
aboutMenu.submenu.pop();
|
||||||
|
} else {
|
||||||
|
throw new Error('updateForMac: help.submenu was not an array!');
|
||||||
|
}
|
||||||
|
|
||||||
// Remove preferences, separator, and quit from the File menu, since they're
|
// Remove preferences, separator, and quit from the File menu, since they're
|
||||||
// in the app menu
|
// in the app menu
|
||||||
const fileMenu = template[0];
|
const fileMenu = template[0];
|
||||||
fileMenu.submenu.pop();
|
if (Array.isArray(fileMenu.submenu)) {
|
||||||
fileMenu.submenu.pop();
|
fileMenu.submenu.pop();
|
||||||
fileMenu.submenu.pop();
|
fileMenu.submenu.pop();
|
||||||
// And insert "close".
|
fileMenu.submenu.pop();
|
||||||
fileMenu.submenu.push(
|
// And insert "close".
|
||||||
{
|
fileMenu.submenu.push(
|
||||||
type: 'separator',
|
{
|
||||||
},
|
type: 'separator',
|
||||||
{
|
},
|
||||||
label: messages.windowMenuClose.message,
|
{
|
||||||
accelerator: 'CmdOrCtrl+W',
|
label: messages.windowMenuClose.message,
|
||||||
role: 'close',
|
accelerator: 'CmdOrCtrl+W',
|
||||||
}
|
role: 'close',
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('updateForMac: fileMenu.submenu was not an array!');
|
||||||
|
}
|
||||||
|
|
||||||
// Add the OSX-specific Signal Desktop menu at the far left
|
// Add the OSX-specific Signal Desktop menu at the far left
|
||||||
template.unshift({
|
template.unshift({
|
||||||
|
@ -273,7 +322,7 @@ function updateForMac(template, messages, options) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: messages.appMenuHideOthers.message,
|
label: messages.appMenuHideOthers.message,
|
||||||
role: 'hideothers',
|
role: 'hideOthers',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: messages.appMenuUnhide.message,
|
label: messages.appMenuUnhide.message,
|
||||||
|
@ -289,25 +338,29 @@ function updateForMac(template, messages, options) {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to Edit menu
|
const editMenu = template[2];
|
||||||
template[2].submenu.push(
|
if (Array.isArray(editMenu.submenu)) {
|
||||||
{
|
editMenu.submenu.push(
|
||||||
type: 'separator',
|
{
|
||||||
},
|
type: 'separator',
|
||||||
{
|
},
|
||||||
label: messages.speech.message,
|
{
|
||||||
submenu: [
|
label: messages.speech.message,
|
||||||
{
|
submenu: [
|
||||||
role: 'startspeaking',
|
{
|
||||||
label: messages.editMenuStartSpeaking.message,
|
role: 'startSpeaking',
|
||||||
},
|
label: messages.editMenuStartSpeaking.message,
|
||||||
{
|
},
|
||||||
role: 'stopspeaking',
|
{
|
||||||
label: messages.editMenuStopSpeaking.message,
|
role: 'stopSpeaking',
|
||||||
},
|
label: messages.editMenuStopSpeaking.message,
|
||||||
],
|
},
|
||||||
}
|
],
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('updateForMac: edit.submenu was not an array!');
|
||||||
|
}
|
||||||
|
|
||||||
// Replace Window menu
|
// Replace Window menu
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
|
@ -4,7 +4,11 @@
|
||||||
// The list of permissions is here:
|
// The list of permissions is here:
|
||||||
// https://electronjs.org/docs/api/session#sessetpermissionrequesthandlerhandler
|
// https://electronjs.org/docs/api/session#sessetpermissionrequesthandlerhandler
|
||||||
|
|
||||||
const PERMISSIONS = {
|
import { session as ElectronSession } from 'electron';
|
||||||
|
|
||||||
|
import { ConfigType } from './config';
|
||||||
|
|
||||||
|
const PERMISSIONS: Record<string, boolean> = {
|
||||||
// Allowed
|
// Allowed
|
||||||
fullscreen: true, // required to show videos in full-screen
|
fullscreen: true, // required to show videos in full-screen
|
||||||
notifications: true, // required to show OS notifications for new messages
|
notifications: true, // required to show OS notifications for new messages
|
||||||
|
@ -19,47 +23,60 @@ const PERMISSIONS = {
|
||||||
pointerLock: false,
|
pointerLock: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function _createPermissionHandler(userConfig) {
|
function _createPermissionHandler(
|
||||||
return (webContents, permission, callback, details) => {
|
userConfig: ConfigType
|
||||||
|
): Parameters<typeof ElectronSession.prototype.setPermissionRequestHandler>[0] {
|
||||||
|
return (_webContents, permission, callback, details): void => {
|
||||||
// We default 'media' permission to false, but the user can override that for
|
// We default 'media' permission to false, but the user can override that for
|
||||||
// the microphone and camera.
|
// the microphone and camera.
|
||||||
if (permission === 'media') {
|
if (permission === 'media') {
|
||||||
if (
|
if (
|
||||||
details.mediaTypes.includes('audio') ||
|
details.mediaTypes?.includes('audio') ||
|
||||||
details.mediaTypes.includes('video')
|
details.mediaTypes?.includes('video')
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
details.mediaTypes.includes('audio') &&
|
details.mediaTypes?.includes('audio') &&
|
||||||
userConfig.get('mediaPermissions')
|
userConfig.get('mediaPermissions')
|
||||||
) {
|
) {
|
||||||
return callback(true);
|
callback(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
details.mediaTypes.includes('video') &&
|
details.mediaTypes?.includes('video') &&
|
||||||
userConfig.get('mediaCameraPermissions')
|
userConfig.get('mediaCameraPermissions')
|
||||||
) {
|
) {
|
||||||
return callback(true);
|
callback(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(false);
|
callback(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it doesn't have 'video' or 'audio', it's probably screenshare.
|
// If it doesn't have 'video' or 'audio', it's probably screenshare.
|
||||||
// TODO: DESKTOP-1611
|
// TODO: DESKTOP-1611
|
||||||
return callback(true);
|
callback(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PERMISSIONS[permission]) {
|
if (PERMISSIONS[permission]) {
|
||||||
console.log(`Approving request for permission '${permission}'`);
|
console.log(`Approving request for permission '${permission}'`);
|
||||||
return callback(true);
|
callback(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Denying request for permission '${permission}'`);
|
console.log(`Denying request for permission '${permission}'`);
|
||||||
return callback(false);
|
callback(false);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function installPermissionsHandler({ session, userConfig }) {
|
export function installPermissionsHandler({
|
||||||
|
session,
|
||||||
|
userConfig,
|
||||||
|
}: {
|
||||||
|
session: typeof ElectronSession;
|
||||||
|
userConfig: ConfigType;
|
||||||
|
}): void {
|
||||||
// Setting the permission request handler to null first forces any permissions to be
|
// Setting the permission request handler to null first forces any permissions to be
|
||||||
// requested again. Without this, revoked permissions might still be available if
|
// requested again. Without this, revoked permissions might still be available if
|
||||||
// they've already been used successfully.
|
// they've already been used successfully.
|
||||||
|
@ -69,7 +86,3 @@ function installPermissionsHandler({ session, userConfig }) {
|
||||||
_createPermissionHandler(userConfig)
|
_createPermissionHandler(userConfig)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
installPermissionsHandler,
|
|
||||||
};
|
|
|
@ -1,10 +1,21 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const path = require('path');
|
import {
|
||||||
const fs = require('fs');
|
protocol as ElectronProtocol,
|
||||||
|
ProtocolRequest,
|
||||||
|
ProtocolResponse,
|
||||||
|
} from 'electron';
|
||||||
|
|
||||||
function _eliminateAllAfterCharacter(string, character) {
|
import { isAbsolute, normalize } from 'path';
|
||||||
|
import { existsSync, realpathSync } from 'fs';
|
||||||
|
|
||||||
|
type CallbackType = (response: string | ProtocolResponse) => void;
|
||||||
|
|
||||||
|
function _eliminateAllAfterCharacter(
|
||||||
|
string: string,
|
||||||
|
character: string
|
||||||
|
): string {
|
||||||
const index = string.indexOf(character);
|
const index = string.indexOf(character);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
return string;
|
return string;
|
||||||
|
@ -13,20 +24,38 @@ function _eliminateAllAfterCharacter(string, character) {
|
||||||
return string.slice(0, index);
|
return string.slice(0, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _urlToPath(targetUrl, options = {}) {
|
export function _urlToPath(
|
||||||
const { isWindows } = options;
|
targetUrl: string,
|
||||||
|
options?: { isWindows: boolean }
|
||||||
|
): string {
|
||||||
const decoded = decodeURIComponent(targetUrl);
|
const decoded = decodeURIComponent(targetUrl);
|
||||||
const withoutScheme = decoded.slice(isWindows ? 8 : 7);
|
const withoutScheme = decoded.slice(options?.isWindows ? 8 : 7);
|
||||||
const withoutQuerystring = _eliminateAllAfterCharacter(withoutScheme, '?');
|
const withoutQuerystring = _eliminateAllAfterCharacter(withoutScheme, '?');
|
||||||
const withoutHash = _eliminateAllAfterCharacter(withoutQuerystring, '#');
|
const withoutHash = _eliminateAllAfterCharacter(withoutQuerystring, '#');
|
||||||
|
|
||||||
return withoutHash;
|
return withoutHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _createFileHandler({ userDataPath, installPath, isWindows }) {
|
function _createFileHandler({
|
||||||
return (request, callback) => {
|
userDataPath,
|
||||||
|
installPath,
|
||||||
|
isWindows,
|
||||||
|
}: {
|
||||||
|
userDataPath: string;
|
||||||
|
installPath: string;
|
||||||
|
isWindows: boolean;
|
||||||
|
}) {
|
||||||
|
return (request: ProtocolRequest, callback: CallbackType): void => {
|
||||||
let targetPath;
|
let targetPath;
|
||||||
|
|
||||||
|
if (!request.url) {
|
||||||
|
// This is an "invalid URL" error. See [Chromium's net error list][0].
|
||||||
|
//
|
||||||
|
// [0]: https://source.chromium.org/chromium/chromium/src/+/master:net/base/net_error_list.h;l=563;drc=a836ee9868cf1b9673fce362a82c98aba3e195de
|
||||||
|
callback({ error: -300 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
targetPath = _urlToPath(request.url, { isWindows });
|
targetPath = _urlToPath(request.url, { isWindows });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -38,24 +67,26 @@ function _createFileHandler({ userDataPath, installPath, isWindows }) {
|
||||||
`Warning: denying request because of an error: ${errorMessage}`
|
`Warning: denying request because of an error: ${errorMessage}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// This is an "invalid URL" error. See [Chromium's net error list][0].
|
callback({ error: -300 });
|
||||||
//
|
return;
|
||||||
// [0]: https://source.chromium.org/chromium/chromium/src/+/master:net/base/net_error_list.h;l=563;drc=a836ee9868cf1b9673fce362a82c98aba3e195de
|
|
||||||
return callback({ error: -300 });
|
|
||||||
}
|
}
|
||||||
// normalize() is primarily useful here for switching / to \ on windows
|
// normalize() is primarily useful here for switching / to \ on windows
|
||||||
const target = path.normalize(targetPath);
|
const target = normalize(targetPath);
|
||||||
// here we attempt to follow symlinks to the ultimate final path, reflective of what
|
// here we attempt to follow symlinks to the ultimate final path, reflective of what
|
||||||
// we do in main.js on userDataPath and installPath
|
// we do in main.js on userDataPath and installPath
|
||||||
const realPath = fs.existsSync(target) ? fs.realpathSync(target) : target;
|
const realPath = existsSync(target) ? realpathSync(target) : target;
|
||||||
// finally we do case-insensitive checks on windows
|
// finally we do case-insensitive checks on windows
|
||||||
const properCasing = isWindows ? realPath.toLowerCase() : realPath;
|
const properCasing = isWindows ? realPath.toLowerCase() : realPath;
|
||||||
|
|
||||||
if (!path.isAbsolute(realPath)) {
|
if (!isAbsolute(realPath)) {
|
||||||
console.log(
|
console.log(
|
||||||
`Warning: denying request to non-absolute path '${realPath}'`
|
`Warning: denying request to non-absolute path '${realPath}'`
|
||||||
);
|
);
|
||||||
return callback();
|
// This is an "Access Denied" error. See [Chromium's net error list][0].
|
||||||
|
//
|
||||||
|
// [0]: https://source.chromium.org/chromium/chromium/src/+/master:net/base/net_error_list.h;l=57;drc=a836ee9868cf1b9673fce362a82c98aba3e195de
|
||||||
|
callback({ error: -10 });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -69,21 +100,27 @@ function _createFileHandler({ userDataPath, installPath, isWindows }) {
|
||||||
console.log(
|
console.log(
|
||||||
`Warning: denying request to path '${realPath}' (userDataPath: '${userDataPath}', installPath: '${installPath}')`
|
`Warning: denying request to path '${realPath}' (userDataPath: '${userDataPath}', installPath: '${installPath}')`
|
||||||
);
|
);
|
||||||
return callback();
|
callback({ error: -10 });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback({
|
callback({
|
||||||
path: realPath,
|
path: realPath,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function installFileHandler({
|
export function installFileHandler({
|
||||||
protocol,
|
protocol,
|
||||||
userDataPath,
|
userDataPath,
|
||||||
installPath,
|
installPath,
|
||||||
isWindows,
|
isWindows,
|
||||||
}) {
|
}: {
|
||||||
|
protocol: typeof ElectronProtocol;
|
||||||
|
userDataPath: string;
|
||||||
|
installPath: string;
|
||||||
|
isWindows: boolean;
|
||||||
|
}): void {
|
||||||
protocol.interceptFileProtocol(
|
protocol.interceptFileProtocol(
|
||||||
'file',
|
'file',
|
||||||
_createFileHandler({ userDataPath, installPath, isWindows })
|
_createFileHandler({ userDataPath, installPath, isWindows })
|
||||||
|
@ -91,11 +128,20 @@ function installFileHandler({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn off browser URI scheme since we do all network requests via Node.js
|
// Turn off browser URI scheme since we do all network requests via Node.js
|
||||||
function _disabledHandler(request, callback) {
|
function _disabledHandler(
|
||||||
return callback();
|
_request: ProtocolRequest,
|
||||||
|
callback: CallbackType
|
||||||
|
): void {
|
||||||
|
callback({ error: -10 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function installWebHandler({ protocol, enableHttp }) {
|
export function installWebHandler({
|
||||||
|
protocol,
|
||||||
|
enableHttp,
|
||||||
|
}: {
|
||||||
|
protocol: typeof ElectronProtocol;
|
||||||
|
enableHttp: string;
|
||||||
|
}): void {
|
||||||
protocol.interceptFileProtocol('about', _disabledHandler);
|
protocol.interceptFileProtocol('about', _disabledHandler);
|
||||||
protocol.interceptFileProtocol('content', _disabledHandler);
|
protocol.interceptFileProtocol('content', _disabledHandler);
|
||||||
protocol.interceptFileProtocol('chrome', _disabledHandler);
|
protocol.interceptFileProtocol('chrome', _disabledHandler);
|
||||||
|
@ -114,9 +160,3 @@ function installWebHandler({ protocol, enableHttp }) {
|
||||||
protocol.interceptFileProtocol('wss', _disabledHandler);
|
protocol.interceptFileProtocol('wss', _disabledHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
_urlToPath,
|
|
||||||
installFileHandler,
|
|
||||||
installWebHandler,
|
|
||||||
};
|
|
|
@ -3,13 +3,20 @@
|
||||||
|
|
||||||
/* eslint-disable strict */
|
/* eslint-disable strict */
|
||||||
|
|
||||||
const { Menu, clipboard, nativeImage } = require('electron');
|
import { BrowserWindow, Menu, clipboard, nativeImage } from 'electron';
|
||||||
const osLocale = require('os-locale');
|
import { sync as osLocaleSync } from 'os-locale';
|
||||||
const { uniq } = require('lodash');
|
import { uniq } from 'lodash';
|
||||||
const url = require('url');
|
import { fileURLToPath } from 'url';
|
||||||
const { maybeParseUrl } = require('../ts/util/url');
|
|
||||||
|
|
||||||
function getLanguages(userLocale, availableLocales) {
|
import { maybeParseUrl } from '../ts/util/url';
|
||||||
|
import { LocaleMessagesType } from '../ts/types/I18N';
|
||||||
|
|
||||||
|
import { MenuListType } from './menu';
|
||||||
|
|
||||||
|
export function getLanguages(
|
||||||
|
userLocale: string,
|
||||||
|
availableLocales: ReadonlyArray<string>
|
||||||
|
): Array<string> {
|
||||||
const baseLocale = userLocale.split('-')[0];
|
const baseLocale = userLocale.split('-')[0];
|
||||||
// Attempt to find the exact locale
|
// Attempt to find the exact locale
|
||||||
const candidateLocales = uniq([userLocale, baseLocale]).filter(l =>
|
const candidateLocales = uniq([userLocale, baseLocale]).filter(l =>
|
||||||
|
@ -25,9 +32,12 @@ function getLanguages(userLocale, availableLocales) {
|
||||||
return uniq(availableLocales.filter(l => l.startsWith(baseLocale)));
|
return uniq(availableLocales.filter(l => l.startsWith(baseLocale)));
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.setup = (browserWindow, messages) => {
|
export const setup = (
|
||||||
|
browserWindow: BrowserWindow,
|
||||||
|
messages: LocaleMessagesType
|
||||||
|
): void => {
|
||||||
const { session } = browserWindow.webContents;
|
const { session } = browserWindow.webContents;
|
||||||
const userLocale = osLocale.sync().replace(/_/g, '-');
|
const userLocale = osLocaleSync().replace(/_/g, '-');
|
||||||
const availableLocales = session.availableSpellCheckerLanguages;
|
const availableLocales = session.availableSpellCheckerLanguages;
|
||||||
const languages = getLanguages(userLocale, availableLocales);
|
const languages = getLanguages(userLocale, availableLocales);
|
||||||
console.log(`spellcheck: user locale: ${userLocale}`);
|
console.log(`spellcheck: user locale: ${userLocale}`);
|
||||||
|
@ -49,7 +59,7 @@ exports.setup = (browserWindow, messages) => {
|
||||||
|
|
||||||
// Popup editor menu
|
// Popup editor menu
|
||||||
if (showMenu) {
|
if (showMenu) {
|
||||||
const template = [];
|
const template: MenuListType = [];
|
||||||
|
|
||||||
if (isMisspelled) {
|
if (isMisspelled) {
|
||||||
if (params.dictionarySuggestions.length > 0) {
|
if (params.dictionarySuggestions.length > 0) {
|
||||||
|
@ -104,7 +114,7 @@ exports.setup = (browserWindow, messages) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = nativeImage.createFromPath(
|
const image = nativeImage.createFromPath(
|
||||||
url.fileURLToPath(params.srcURL)
|
fileURLToPath(params.srcURL)
|
||||||
);
|
);
|
||||||
clipboard.writeImage(image);
|
clipboard.writeImage(image);
|
||||||
};
|
};
|
||||||
|
@ -136,14 +146,14 @@ exports.setup = (browserWindow, messages) => {
|
||||||
if (editFlags.canSelectAll && params.isEditable) {
|
if (editFlags.canSelectAll && params.isEditable) {
|
||||||
template.push({
|
template.push({
|
||||||
label: messages.editMenuSelectAll.message,
|
label: messages.editMenuSelectAll.message,
|
||||||
role: 'selectall',
|
role: 'selectAll',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(template);
|
const menu = Menu.buildFromTemplate(template);
|
||||||
menu.popup(browserWindow);
|
menu.popup({
|
||||||
|
window: browserWindow,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getLanguages = getLanguages;
|
|
|
@ -1,24 +1,23 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const electron = require('electron');
|
import { ipcMain } from 'electron';
|
||||||
const { remove: removeUserConfig } = require('./user_config');
|
|
||||||
const { remove: removeEphemeralConfig } = require('./ephemeral_config');
|
|
||||||
|
|
||||||
const { ipcMain } = electron;
|
import { remove as removeUserConfig } from './user_config';
|
||||||
|
import { remove as removeEphemeralConfig } from './ephemeral_config';
|
||||||
|
|
||||||
let sql;
|
type SQLType = {
|
||||||
|
sqlCall(callName: string, args: ReadonlyArray<unknown>): unknown;
|
||||||
module.exports = {
|
|
||||||
initialize,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let sql: SQLType | undefined;
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
const SQL_CHANNEL_KEY = 'sql-channel';
|
const SQL_CHANNEL_KEY = 'sql-channel';
|
||||||
const ERASE_SQL_KEY = 'erase-sql-key';
|
const ERASE_SQL_KEY = 'erase-sql-key';
|
||||||
|
|
||||||
function initialize(mainSQL) {
|
export function initialize(mainSQL: SQLType): void {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
throw new Error('sqlChannels: already initialized!');
|
throw new Error('sqlChannels: already initialized!');
|
||||||
}
|
}
|
||||||
|
@ -28,6 +27,9 @@ function initialize(mainSQL) {
|
||||||
|
|
||||||
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
|
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
|
||||||
try {
|
try {
|
||||||
|
if (!sql) {
|
||||||
|
throw new Error(`${SQL_CHANNEL_KEY}: Not yet initialized!`);
|
||||||
|
}
|
||||||
const result = await sql.sqlCall(callName, args);
|
const result = await sql.sqlCall(callName, args);
|
||||||
event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, null, result);
|
event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, null, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
|
@ -1,17 +1,22 @@
|
||||||
// Copyright 2017-2021 Signal Messenger, LLC
|
// Copyright 2017-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const path = require('path');
|
import { join } from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
const fs = require('fs');
|
import { BrowserWindow, app, Menu, Tray } from 'electron';
|
||||||
const { app, Menu, Tray } = require('electron');
|
import * as DockIcon from '../ts/dock_icon';
|
||||||
const dockIcon = require('../ts/dock_icon');
|
|
||||||
|
import { LocaleMessagesType } from '../ts/types/I18N';
|
||||||
|
|
||||||
let trayContextMenu = null;
|
let trayContextMenu = null;
|
||||||
let tray = null;
|
let tray: Tray | undefined;
|
||||||
|
|
||||||
function createTrayIcon(getMainWindow, messages) {
|
export default function createTrayIcon(
|
||||||
let iconSize;
|
getMainWindow: () => BrowserWindow | undefined,
|
||||||
|
messages: LocaleMessagesType
|
||||||
|
): { updateContextMenu: () => void; updateIcon: (count: number) => void } {
|
||||||
|
let iconSize: string;
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case 'darwin':
|
case 'darwin':
|
||||||
iconSize = '16';
|
iconSize = '16';
|
||||||
|
@ -24,7 +29,7 @@ function createTrayIcon(getMainWindow, messages) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconNoNewMessages = path.join(
|
const iconNoNewMessages = join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'..',
|
||||||
'images',
|
'images',
|
||||||
|
@ -33,7 +38,7 @@ function createTrayIcon(getMainWindow, messages) {
|
||||||
|
|
||||||
tray = new Tray(iconNoNewMessages);
|
tray = new Tray(iconNoNewMessages);
|
||||||
|
|
||||||
tray.forceOnTop = mainWindow => {
|
const forceOnTop = (mainWindow: BrowserWindow) => {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
// On some versions of GNOME the window may not be on top when restored.
|
// On some versions of GNOME the window may not be on top when restored.
|
||||||
// This trick should fix it.
|
// This trick should fix it.
|
||||||
|
@ -44,35 +49,35 @@ function createTrayIcon(getMainWindow, messages) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tray.toggleWindowVisibility = () => {
|
const toggleWindowVisibility = () => {
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = getMainWindow();
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
if (mainWindow.isVisible()) {
|
if (mainWindow.isVisible()) {
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
dockIcon.hide();
|
DockIcon.hide();
|
||||||
} else {
|
} else {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
dockIcon.show();
|
DockIcon.show();
|
||||||
|
|
||||||
tray.forceOnTop(mainWindow);
|
forceOnTop(mainWindow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tray.updateContextMenu();
|
updateContextMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
tray.showWindow = () => {
|
const showWindow = () => {
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = getMainWindow();
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
if (!mainWindow.isVisible()) {
|
if (!mainWindow.isVisible()) {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
tray.forceOnTop(mainWindow);
|
forceOnTop(mainWindow);
|
||||||
}
|
}
|
||||||
tray.updateContextMenu();
|
updateContextMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
tray.updateContextMenu = () => {
|
const updateContextMenu = () => {
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = getMainWindow();
|
||||||
|
|
||||||
// NOTE: we want to have the show/hide entry available in the tray icon
|
// NOTE: we want to have the show/hide entry available in the tray icon
|
||||||
|
@ -85,7 +90,7 @@ function createTrayIcon(getMainWindow, messages) {
|
||||||
label:
|
label:
|
||||||
messages[mainWindow && mainWindow.isVisible() ? 'hide' : 'show']
|
messages[mainWindow && mainWindow.isVisible() ? 'hide' : 'show']
|
||||||
.message,
|
.message,
|
||||||
click: tray.toggleWindowVisibility,
|
click: toggleWindowVisibility,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'quit',
|
id: 'quit',
|
||||||
|
@ -94,25 +99,25 @@ function createTrayIcon(getMainWindow, messages) {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
tray.setContextMenu(trayContextMenu);
|
tray?.setContextMenu(trayContextMenu);
|
||||||
};
|
};
|
||||||
|
|
||||||
tray.updateIcon = unreadCount => {
|
const updateIcon = (unreadCount: number) => {
|
||||||
let image;
|
let image;
|
||||||
|
|
||||||
if (unreadCount > 0) {
|
if (unreadCount > 0) {
|
||||||
const filename = `${String(unreadCount >= 10 ? 10 : unreadCount)}.png`;
|
const filename = `${String(unreadCount >= 10 ? 10 : unreadCount)}.png`;
|
||||||
image = path.join(__dirname, '..', 'images', 'alert', iconSize, filename);
|
image = join(__dirname, '..', 'images', 'alert', iconSize, filename);
|
||||||
} else {
|
} else {
|
||||||
image = iconNoNewMessages;
|
image = iconNoNewMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(image)) {
|
if (!existsSync(image)) {
|
||||||
console.log('tray.updateIcon: Image for tray update does not exist!');
|
console.log('tray.updateIcon: Image for tray update does not exist!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
tray.setImage(image);
|
tray?.setImage(image);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(
|
console.log(
|
||||||
'tray.setImage error:',
|
'tray.setImage error:',
|
||||||
|
@ -121,12 +126,13 @@ function createTrayIcon(getMainWindow, messages) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tray.on('click', tray.showWindow);
|
tray.on('click', showWindow);
|
||||||
|
|
||||||
tray.setToolTip(messages.signalDesktop.message);
|
tray.setToolTip(messages.signalDesktop.message);
|
||||||
tray.updateContextMenu();
|
updateContextMenu();
|
||||||
|
|
||||||
return tray;
|
return {
|
||||||
|
updateContextMenu,
|
||||||
|
updateIcon,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = createTrayIcon;
|
|
4
app/user_config.d.ts
vendored
4
app/user_config.d.ts
vendored
|
@ -1,4 +0,0 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
export function remove(): void;
|
|
|
@ -1,16 +1,15 @@
|
||||||
// Copyright 2017-2020 Signal Messenger, LLC
|
// Copyright 2017-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const path = require('path');
|
import { join } from 'path';
|
||||||
|
import { app } from 'electron';
|
||||||
|
|
||||||
const { app } = require('electron');
|
import { start } from './base_config';
|
||||||
|
import config from './config';
|
||||||
const { start } = require('./base_config');
|
|
||||||
const config = require('./config');
|
|
||||||
|
|
||||||
// Use separate data directory for development
|
// Use separate data directory for development
|
||||||
if (config.has('storageProfile')) {
|
if (config.has('storageProfile')) {
|
||||||
const userData = path.join(
|
const userData = join(
|
||||||
app.getPath('appData'),
|
app.getPath('appData'),
|
||||||
`Signal-${config.get('storageProfile')}`
|
`Signal-${config.get('storageProfile')}`
|
||||||
);
|
);
|
||||||
|
@ -21,8 +20,10 @@ if (config.has('storageProfile')) {
|
||||||
console.log(`userData: ${app.getPath('userData')}`);
|
console.log(`userData: ${app.getPath('userData')}`);
|
||||||
|
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
const targetPath = path.join(userDataPath, 'config.json');
|
const targetPath = join(userDataPath, 'config.json');
|
||||||
|
|
||||||
const userConfig = start('user', targetPath);
|
const userConfig = start('user', targetPath);
|
||||||
|
|
||||||
module.exports = userConfig;
|
export const get = userConfig.get.bind(userConfig);
|
||||||
|
export const remove = userConfig.remove.bind(userConfig);
|
||||||
|
export const set = userConfig.set.bind(userConfig);
|
5
app/window_state.d.ts
vendored
5
app/window_state.d.ts
vendored
|
@ -1,5 +0,0 @@
|
||||||
// Copyright 2019-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
export function markShouldQuit(): void;
|
|
||||||
export function shouldQuit(): void;
|
|
|
@ -3,15 +3,10 @@
|
||||||
|
|
||||||
let shouldQuitFlag = false;
|
let shouldQuitFlag = false;
|
||||||
|
|
||||||
function markShouldQuit() {
|
export function markShouldQuit(): void {
|
||||||
shouldQuitFlag = true;
|
shouldQuitFlag = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldQuit() {
|
export function shouldQuit(): boolean {
|
||||||
return shouldQuitFlag;
|
return shouldQuitFlag;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
shouldQuit,
|
|
||||||
markShouldQuit,
|
|
||||||
};
|
|
6
main.js
6
main.js
|
@ -71,7 +71,7 @@ const startInTray = process.argv.some(arg => arg === '--start-in-tray');
|
||||||
const usingTrayIcon =
|
const usingTrayIcon =
|
||||||
startInTray || process.argv.some(arg => arg === '--use-tray-icon');
|
startInTray || process.argv.some(arg => arg === '--use-tray-icon');
|
||||||
|
|
||||||
const config = require('./app/config');
|
const config = require('./app/config').default;
|
||||||
|
|
||||||
// Very important to put before the single instance check, since it is based on the
|
// Very important to put before the single instance check, since it is based on the
|
||||||
// userData directory.
|
// userData directory.
|
||||||
|
@ -91,7 +91,7 @@ const attachments = require('./app/attachments');
|
||||||
const attachmentChannel = require('./app/attachment_channel');
|
const attachmentChannel = require('./app/attachment_channel');
|
||||||
const bounce = require('./ts/services/bounce');
|
const bounce = require('./ts/services/bounce');
|
||||||
const updater = require('./ts/updater/index');
|
const updater = require('./ts/updater/index');
|
||||||
const createTrayIcon = require('./app/tray_icon');
|
const createTrayIcon = require('./app/tray_icon').default;
|
||||||
const dockIcon = require('./ts/dock_icon');
|
const dockIcon = require('./ts/dock_icon');
|
||||||
const ephemeralConfig = require('./app/ephemeral_config');
|
const ephemeralConfig = require('./app/ephemeral_config');
|
||||||
const logging = require('./ts/logging/main_process_logging');
|
const logging = require('./ts/logging/main_process_logging');
|
||||||
|
@ -1353,7 +1353,7 @@ app.on('ready', async () => {
|
||||||
);
|
);
|
||||||
await attachments.deleteAllDraftAttachments({
|
await attachments.deleteAllDraftAttachments({
|
||||||
userDataPath,
|
userDataPath,
|
||||||
stickers: orphanedDraftAttachments,
|
attachments: orphanedDraftAttachments,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,6 @@
|
||||||
"tar": "4.4.8",
|
"tar": "4.4.8",
|
||||||
"testcheck": "1.0.0-rc.2",
|
"testcheck": "1.0.0-rc.2",
|
||||||
"tmp": "0.0.33",
|
"tmp": "0.0.33",
|
||||||
"to-arraybuffer": "1.0.1",
|
|
||||||
"typeface-inter": "3.10.0",
|
"typeface-inter": "3.10.0",
|
||||||
"underscore": "1.12.1",
|
"underscore": "1.12.1",
|
||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Hide Others",
|
"label": "Hide Others",
|
||||||
"role": "hideothers"
|
"role": "hideOthers"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Show All",
|
"label": "Show All",
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Paste and Match Style",
|
"label": "Paste and Match Style",
|
||||||
"role": "pasteandmatchstyle"
|
"role": "pasteAndMatchStyle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Delete",
|
"label": "Delete",
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Select All",
|
"label": "Select All",
|
||||||
"role": "selectall"
|
"role": "selectAll"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "separator"
|
"type": "separator"
|
||||||
|
@ -115,11 +115,11 @@
|
||||||
"submenu": [
|
"submenu": [
|
||||||
{
|
{
|
||||||
"label": "Start speaking",
|
"label": "Start speaking",
|
||||||
"role": "startspeaking"
|
"role": "startSpeaking"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Stop speaking",
|
"label": "Stop speaking",
|
||||||
"role": "stopspeaking"
|
"role": "stopSpeaking"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -130,16 +130,16 @@
|
||||||
"submenu": [
|
"submenu": [
|
||||||
{
|
{
|
||||||
"label": "Actual Size",
|
"label": "Actual Size",
|
||||||
"role": "resetzoom"
|
"role": "resetZoom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"accelerator": "Command+=",
|
"accelerator": "Command+=",
|
||||||
"label": "Zoom In",
|
"label": "Zoom In",
|
||||||
"role": "zoomin"
|
"role": "zoomIn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Zoom Out",
|
"label": "Zoom Out",
|
||||||
"role": "zoomout"
|
"role": "zoomOut"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "separator"
|
"type": "separator"
|
||||||
|
@ -160,7 +160,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Toggle Developer Tools",
|
"label": "Toggle Developer Tools",
|
||||||
"role": "toggledevtools"
|
"role": "toggleDevTools"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Hide Others",
|
"label": "Hide Others",
|
||||||
"role": "hideothers"
|
"role": "hideOthers"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Show All",
|
"label": "Show All",
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Paste and Match Style",
|
"label": "Paste and Match Style",
|
||||||
"role": "pasteandmatchstyle"
|
"role": "pasteAndMatchStyle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Delete",
|
"label": "Delete",
|
||||||
|
@ -98,7 +98,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Select All",
|
"label": "Select All",
|
||||||
"role": "selectall"
|
"role": "selectAll"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "separator"
|
"type": "separator"
|
||||||
|
@ -108,11 +108,11 @@
|
||||||
"submenu": [
|
"submenu": [
|
||||||
{
|
{
|
||||||
"label": "Start speaking",
|
"label": "Start speaking",
|
||||||
"role": "startspeaking"
|
"role": "startSpeaking"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Stop speaking",
|
"label": "Stop speaking",
|
||||||
"role": "stopspeaking"
|
"role": "stopSpeaking"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -123,16 +123,16 @@
|
||||||
"submenu": [
|
"submenu": [
|
||||||
{
|
{
|
||||||
"label": "Actual Size",
|
"label": "Actual Size",
|
||||||
"role": "resetzoom"
|
"role": "resetZoom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"accelerator": "Command+=",
|
"accelerator": "Command+=",
|
||||||
"label": "Zoom In",
|
"label": "Zoom In",
|
||||||
"role": "zoomin"
|
"role": "zoomIn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Zoom Out",
|
"label": "Zoom Out",
|
||||||
"role": "zoomout"
|
"role": "zoomOut"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "separator"
|
"type": "separator"
|
||||||
|
@ -153,7 +153,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Toggle Developer Tools",
|
"label": "Toggle Developer Tools",
|
||||||
"role": "toggledevtools"
|
"role": "toggleDevTools"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Paste and Match Style",
|
"label": "Paste and Match Style",
|
||||||
"role": "pasteandmatchstyle"
|
"role": "pasteAndMatchStyle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Delete",
|
"label": "Delete",
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Select All",
|
"label": "Select All",
|
||||||
"role": "selectall"
|
"role": "selectAll"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -72,16 +72,16 @@
|
||||||
"submenu": [
|
"submenu": [
|
||||||
{
|
{
|
||||||
"label": "Actual Size",
|
"label": "Actual Size",
|
||||||
"role": "resetzoom"
|
"role": "resetZoom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"accelerator": "Control+=",
|
"accelerator": "Control+=",
|
||||||
"label": "Zoom In",
|
"label": "Zoom In",
|
||||||
"role": "zoomin"
|
"role": "zoomIn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Zoom Out",
|
"label": "Zoom Out",
|
||||||
"role": "zoomout"
|
"role": "zoomOut"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "separator"
|
"type": "separator"
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Toggle Developer Tools",
|
"label": "Toggle Developer Tools",
|
||||||
"role": "toggledevtools"
|
"role": "toggleDevTools"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Paste and Match Style",
|
"label": "Paste and Match Style",
|
||||||
"role": "pasteandmatchstyle"
|
"role": "pasteAndMatchStyle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Delete",
|
"label": "Delete",
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Select All",
|
"label": "Select All",
|
||||||
"role": "selectall"
|
"role": "selectAll"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -65,16 +65,16 @@
|
||||||
"submenu": [
|
"submenu": [
|
||||||
{
|
{
|
||||||
"label": "Actual Size",
|
"label": "Actual Size",
|
||||||
"role": "resetzoom"
|
"role": "resetZoom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"accelerator": "Control+=",
|
"accelerator": "Control+=",
|
||||||
"label": "Zoom In",
|
"label": "Zoom In",
|
||||||
"role": "zoomin"
|
"role": "zoomIn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Zoom Out",
|
"label": "Zoom Out",
|
||||||
"role": "zoomout"
|
"role": "zoomOut"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "separator"
|
"type": "separator"
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Toggle Developer Tools",
|
"label": "Toggle Developer Tools",
|
||||||
"role": "toggledevtools"
|
"role": "toggleDevTools"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
Manual test script
|
|
||||||
|
|
||||||
Some things are very difficult to test programmatically. Also, if you don't have adequate test coverage, a good first step is a comprehensive manual test script! https://blog.scottnonnenberg.com/web-application-test-strategy/
|
|
||||||
|
|
||||||
Conversation view:
|
|
||||||
Last seen indicator:
|
|
||||||
(dismissed three ways: 1. sending a message 2. switching away from conversation and back again 3. clicking scroll down button when last seen indicator is off-screen above)
|
|
||||||
|
|
||||||
- Switch away from Signal app, but keep it visible
|
|
||||||
- Receive messages to conversation out of focus, and the last seen indicator should move up the screen with each new message. When the number of new messages can no longer fit on the screen, the last seen indicator should stay at the top of the screen, and new messages will appear below. The scroll down button will turn blue to indicate new messages out of view.
|
|
||||||
- Switch back to Signal app, and the last seen indicator and scroll down button should stay where they are.
|
|
||||||
- Click the scroll down button to go to the bottom of the window, and the button should disappear.
|
|
||||||
- Send a message, then scroll up. The last seen indicator should be gone.
|
|
||||||
|
|
||||||
- Switch to a different conversation, then receive messages on original conversation
|
|
||||||
- Switch back to original conversation, and the last seen indicator should be visible
|
|
||||||
- Switch away from conversation and back. The last seen indicator should be gone.
|
|
||||||
|
|
||||||
- Switch to a different conversation, then receive a lot of messages on original conversation
|
|
||||||
- Switch back to original conversation, and the last seen indicator should be visible, along with the scroll down button.
|
|
||||||
- Click the scroll down button to be taken to the newest message in the conversation
|
|
||||||
|
|
||||||
- Scroll up on a conversation then switch to another application, keeping the Signal application visible. Receive new messages on that conversation. Switch back to application. The scroll down button should be blue, and the conversation scroll location should stay where it was. There should be a last seen indicator visible above the new messages.
|
|
||||||
|
|
||||||
- Scroll to bottom of a conversation, then switch to another application, keeping Signal application visible. Receive new messages on that conversation. As new messages come in, the last seen indicator should march up the screen. Before it reaches the top, switch back to the application. This will mark those messages as read. Switch away from the application again, and receive new messages. The last seen indicator will scroll off the top of the screen as more and more new messages come in.
|
|
||||||
|
|
||||||
- ADVANCED: Set up an automated script (or friend) to send you repeated messages. You should see the right number of unread upon entry of the conversation, along with with the last seen indicator. While the conversation is focused, new messages should increment the last seen indicator until it is offscreen above. Click the scroll down button to eliminate the last seen indicator, then scroll up. New messages received while scrolled up should not scroll the conversation, but will add a new last seen indicator and scroll down button.
|
|
||||||
|
|
||||||
- ADVANCED: Set fetch limit to a low number, like 3 (in models/messages.js, fetchConversation function). Load the application, and don't select the conversation. Receive more than four new messages in that conversation. Select the conversation. The last seen indicator should reflect the total number of new messages and all of them should be visible.
|
|
||||||
|
|
||||||
Marking messages as unread:
|
|
||||||
- Switch to a different conversation, then receive lots of messages on original conversation, more than would fill the screen
|
|
||||||
- Note the count before clicking into the conversation. Count the number of visible messages and ensure that the conversation's unread count is decremented by the right amount.
|
|
||||||
- Slowly scroll down so that one more message is visible. The conversation unread count should go down by one.
|
|
||||||
- Click the scroll down button. All messages should be marked read - even if you skipped a couple screens to get to the bottom.
|
|
||||||
|
|
||||||
Scrolling:
|
|
||||||
- If scrolled to bottom of a conversation, should stay there when a new message comes in
|
|
||||||
- If scrolled to the middle of a conversation, should stay there when a new message comes in
|
|
||||||
- When you've scrolled up an entire screen's worth, a scroll down button in the bottom right should appear.
|
|
||||||
|
|
||||||
Scroll-down button:
|
|
||||||
- Clicking it takes you to the bottom of the conversation, makes the button disappear
|
|
||||||
- If a new message comes in while it is already showing, it turns blue
|
|
||||||
- If a new message comes in while not at the bottom of the conversation (but button is not already showing), it should appear, already blue.
|
|
||||||
- If you've scrolled up higher than the last seen indicator, then clicking the scroll down button should take you to the last seen indicator. Once there, clicking the button will take you to the bottom of the conversation, at which point the button will disappear.
|
|
||||||
|
|
||||||
Electron window locations
|
|
||||||
- Load app, move and resize window, close app. Start app. Window should be in the same place, with the same size.
|
|
||||||
- (OSX) Load app, full-screen window, close app. Start app. Window should be full screen.
|
|
||||||
- (Windows) Load app, maximize window, close app. Start app. Window should be maximized.
|
|
10
ts/os-locale.d.ts
vendored
Normal file
10
ts/os-locale.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// We need this until we upgrade os-locale. Newer versions include type definitions.
|
||||||
|
|
||||||
|
// We can't upgrade it yet because we patch it to disable its findup/exec behavior.
|
||||||
|
|
||||||
|
declare module 'os-locale' {
|
||||||
|
export function sync(): string;
|
||||||
|
}
|
|
@ -12,10 +12,12 @@ export type ReplacementValuesType<T> = {
|
||||||
[key: string]: T;
|
[key: string]: T;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LocalizerType = (
|
||||||
|
key: string,
|
||||||
|
placeholders: Array<string> | ReplacementValuesType<string>
|
||||||
|
) => string;
|
||||||
|
|
||||||
export type LocaleType = {
|
export type LocaleType = {
|
||||||
i18n: (
|
i18n: LocalizerType;
|
||||||
key: string,
|
|
||||||
placeholders: Array<string> | ReplacementValuesType<string>
|
|
||||||
) => string;
|
|
||||||
messages: LocaleMessagesType;
|
messages: LocaleMessagesType;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
[
|
[
|
||||||
{
|
|
||||||
"rule": "jQuery-load(",
|
|
||||||
"path": "app/locale.js",
|
|
||||||
"line": "function load({ appLocale, logger } = {}) {",
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-13T21:20:44.234Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-after(",
|
"rule": "jQuery-after(",
|
||||||
"path": "components/indexeddb-backbonejs-adapter/backbone-indexeddb.js",
|
"path": "components/indexeddb-backbonejs-adapter/backbone-indexeddb.js",
|
||||||
|
|
|
@ -28,6 +28,7 @@ const excludedFilesRegexps = [
|
||||||
'\\.d\\.ts$',
|
'\\.d\\.ts$',
|
||||||
|
|
||||||
// High-traffic files in our project
|
// High-traffic files in our project
|
||||||
|
'^app/.+(ts|js)',
|
||||||
'^ts/models/messages.js',
|
'^ts/models/messages.js',
|
||||||
'^ts/models/messages.ts',
|
'^ts/models/messages.ts',
|
||||||
'^ts/models/conversations.js',
|
'^ts/models/conversations.js',
|
||||||
|
|
|
@ -57,6 +57,7 @@
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
|
"app/*",
|
||||||
"node_modules/zkgroup/zkgroup/modules/*",
|
"node_modules/zkgroup/zkgroup/modules/*",
|
||||||
"package.json"
|
"package.json"
|
||||||
]
|
]
|
||||||
|
|
|
@ -17675,7 +17675,7 @@ tmp@0.1.0, tmp@^0.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
rimraf "^2.6.3"
|
rimraf "^2.6.3"
|
||||||
|
|
||||||
to-arraybuffer@1.0.1, to-arraybuffer@^1.0.0:
|
to-arraybuffer@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
|
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
|
||||||
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
|
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue