Merge branch 'main' into HEAD
This commit is contained in:
commit
d57d0cea19
1135 changed files with 264116 additions and 302492 deletions
|
@ -20,6 +20,7 @@ build/ICUMessageParams.d.ts
|
|||
# Third-party files
|
||||
js/Mp3LameEncoder.min.js
|
||||
js/WebAudioRecorderMp3.js
|
||||
js/calling-tools/**
|
||||
|
||||
# TypeScript generated files
|
||||
app/**/*.js
|
||||
|
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -17,7 +17,7 @@ Remember, you can preview this before saving it.
|
|||
- [ ] My contribution is **not** related to translations.
|
||||
- [ ] My commits are in nice logical chunks with [good commit messages](http://chris.beams.io/posts/git-commit/)
|
||||
- [ ] My changes are [rebased](https://medium.com/free-code-camp/git-rebase-and-the-golden-rule-explained-70715eccc372) on the latest [`main`](https://github.com/signalapp/Signal-Desktop/tree/main) branch
|
||||
- [ ] A `yarn ready` run passes successfully ([more about tests here](https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md#tests))
|
||||
- [ ] A `npm run ready` run passes successfully ([more about tests here](https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md#tests))
|
||||
- [ ] My changes are ready to be shipped to users
|
||||
|
||||
### Description
|
||||
|
|
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
repository: signalapp/Signal-Backport-Action-Private
|
||||
|
|
26
.github/workflows/benchmark.yml
vendored
26
.github/workflows/benchmark.yml
vendored
|
@ -9,11 +9,13 @@ on:
|
|||
- main
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 */12 * * *'
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && (!github.event.schedule || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
|
@ -23,35 +25,35 @@ jobs:
|
|||
run: uname -a
|
||||
|
||||
- name: Clone Desktop repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.9.0'
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install global dependencies
|
||||
run: npm install -g yarn@1.22.10 npm@10.2.5
|
||||
run: npm install -g npm@10.2.5
|
||||
|
||||
- name: Install xvfb
|
||||
run: sudo apt-get install xvfb
|
||||
|
||||
- name: Cache Desktop node_modules
|
||||
id: cache-desktop-modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', 'patches/**') }}
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
run: npm ci
|
||||
env:
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
|
||||
- name: Build typescript
|
||||
run: yarn generate
|
||||
run: npm run generate
|
||||
- name: Bundle
|
||||
run: yarn build:esbuild:prod
|
||||
run: npm run build:esbuild:prod
|
||||
|
||||
- name: Run startup benchmarks
|
||||
run: |
|
||||
|
@ -128,13 +130,13 @@ jobs:
|
|||
|
||||
- name: Upload benchmark logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: logs
|
||||
path: artifacts
|
||||
|
||||
- name: Clone benchmark repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'signalapp/Signal-Desktop-Benchmarks-Private'
|
||||
path: 'benchmark-results'
|
||||
|
|
191
.github/workflows/ci.yml
vendored
191
.github/workflows/ci.yml
vendored
|
@ -18,48 +18,49 @@ jobs:
|
|||
steps:
|
||||
- run: lsb_release -a
|
||||
- run: uname -a
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.9.0'
|
||||
- run: npm install -g yarn@1.22.10 npm@10.2.5
|
||||
|
||||
- name: Cache Desktop node_modules
|
||||
id: cache-desktop-modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', 'patches/**') }}
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm install -g npm@10.2.5
|
||||
|
||||
- name: Restore cached .eslintcache and tsconfig.tsbuildinfo
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
id: cache-lint
|
||||
with:
|
||||
path: |
|
||||
.eslintcache
|
||||
tsconfig.tsbuildinfo
|
||||
key: lint-${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', 'patches/**', '.eslintrc.js', '.eslint/**', 'tsconfig.json') }}
|
||||
key: lint-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**', '.eslintrc.js', '.eslint/**', 'tsconfig.json') }}
|
||||
|
||||
- name: Cache Desktop node_modules
|
||||
id: cache-desktop-modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
run: npm ci
|
||||
env:
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
|
||||
- run: yarn generate
|
||||
- run: yarn lint
|
||||
- run: yarn lint-deps
|
||||
- run: yarn lint-license-comments
|
||||
- run: npm run generate
|
||||
- run: npm run lint
|
||||
- run: npm run lint-deps
|
||||
- run: npm run lint-license-comments
|
||||
|
||||
- name: Check acknowledgments file is up to date
|
||||
run: yarn build:acknowledgments
|
||||
run: npm run build:acknowledgments
|
||||
env:
|
||||
REQUIRE_SIGNAL_LIB_FILES: 1
|
||||
|
||||
- run: git diff --exit-code
|
||||
|
||||
- name: Update cached .eslintcache and tsconfig.tsbuildinfo
|
||||
uses: actions/cache/save@v3
|
||||
uses: actions/cache/save@v4
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: |
|
||||
|
@ -75,109 +76,113 @@ jobs:
|
|||
|
||||
steps:
|
||||
- run: uname -a
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.9.0'
|
||||
|
||||
- run: npm install -g yarn@1.22.10 npm@10.2.5
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- run: npm install -g npm@10.2.5
|
||||
|
||||
- name: Cache Desktop node_modules
|
||||
id: cache-desktop-modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', 'patches/**') }}
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
run: npm ci
|
||||
env:
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
|
||||
- run: yarn generate
|
||||
- run: yarn prepare-beta-build
|
||||
- run: yarn test-node
|
||||
- run: yarn test-electron
|
||||
- run: npm run generate
|
||||
- run: npm run prepare-beta-build
|
||||
- run: npm run test-node
|
||||
- run: npm run test-electron
|
||||
env:
|
||||
ARTIFACTS_DIR: artifacts/macos
|
||||
timeout-minutes: 5
|
||||
- run: touch noop.sh && chmod +x noop.sh
|
||||
- run: yarn build
|
||||
- run: npm run build
|
||||
env:
|
||||
DISABLE_INSPECT_FUSE: on
|
||||
SIGN_MACOS_SCRIPT: noop.sh
|
||||
- name: Rebuild native modules for x64
|
||||
run: yarn electron:install-app-deps
|
||||
- run: yarn test-release
|
||||
run: npm run electron:install-app-deps
|
||||
- run: npm run test-release
|
||||
env:
|
||||
NODE_ENV: production
|
||||
- run: yarn test-eslint
|
||||
- run: npm run test-eslint
|
||||
|
||||
- name: Upload artifacts on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
linux:
|
||||
needs: lint
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- run: lsb_release -a
|
||||
- run: uname -a
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.9.0'
|
||||
node-version-file: '.nvmrc'
|
||||
- run: sudo apt-get install xvfb
|
||||
|
||||
- run: npm install -g yarn@1.22.10 npm@10.2.5
|
||||
- run: npm install -g npm@10.2.5
|
||||
|
||||
- name: Cache Desktop node_modules
|
||||
id: cache-desktop-modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', 'patches/**') }}
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
run: npm ci
|
||||
env:
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
|
||||
- run: yarn generate
|
||||
- run: yarn prepare-beta-build
|
||||
- run: npm run generate
|
||||
- run: npm run prepare-beta-build
|
||||
|
||||
- name: Create bundle
|
||||
run: yarn build:esbuild:prod
|
||||
run: npm run build:esbuild:prod
|
||||
|
||||
- name: Build with packaging .deb file
|
||||
run: yarn build:release -- --publish=never
|
||||
run: npm run build:release -- --publish=never
|
||||
if: github.ref == 'refs/heads/main'
|
||||
env:
|
||||
DISABLE_INSPECT_FUSE: on
|
||||
- name: Build without packaging .deb file
|
||||
run: yarn build:release --linux dir
|
||||
run: npm run build:release -- --linux dir
|
||||
if: github.ref != 'refs/heads/main'
|
||||
env:
|
||||
DISABLE_INSPECT_FUSE: on
|
||||
|
||||
- run: xvfb-run --auto-servernum yarn test-node
|
||||
- run: xvfb-run --auto-servernum yarn test-electron
|
||||
- run: xvfb-run --auto-servernum npm run test-node
|
||||
- run: xvfb-run --auto-servernum npm run test-electron
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
ARTIFACTS_DIR: artifacts/linux
|
||||
LANG: en_US
|
||||
LANGUAGE: en_US
|
||||
- run: xvfb-run --auto-servernum yarn test-release
|
||||
- run: xvfb-run --auto-servernum npm run test-release
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Upload artifacts on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
|
@ -194,11 +199,12 @@ jobs:
|
|||
- run: systeminfo
|
||||
- run: git config --global core.autocrlf false
|
||||
- run: git config --global core.eol lf
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.9.0'
|
||||
- run: npm install -g yarn@1.22.10 npm@10.2.5 node-gyp@10.0.1
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm install -g npm@10.2.5 node-gyp@10.0.1
|
||||
|
||||
# Set things up so @nodert-win10-rs4 dependencies build properly
|
||||
- run: dir "$env:BUILD_LOCATION"
|
||||
|
@ -208,49 +214,50 @@ jobs:
|
|||
|
||||
- name: Cache Desktop node_modules
|
||||
id: cache-desktop-modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', 'patches/**') }}
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
run: npm ci
|
||||
env:
|
||||
CHILD_CONCURRENCY: 1
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
|
||||
- run: yarn generate
|
||||
- run: yarn test-node
|
||||
- run: npm run generate
|
||||
- run: npm run test-node
|
||||
- run: copy package.json temp.json
|
||||
- run: del package.json
|
||||
- run: type temp.json | findstr /v certificateSubjectName | findstr /v certificateSha1 > package.json
|
||||
- run: yarn prepare-beta-build
|
||||
- run: npm run prepare-beta-build
|
||||
|
||||
- name: Create bundle
|
||||
run: yarn build:esbuild:prod
|
||||
run: npm run build:esbuild:prod
|
||||
|
||||
- name: Build with NSIS
|
||||
run: yarn build:release
|
||||
run: npm run build:release
|
||||
if: github.ref == 'refs/heads/main'
|
||||
env:
|
||||
DISABLE_INSPECT_FUSE: on
|
||||
- name: Build without NSIS
|
||||
run: yarn build:release --win dir
|
||||
run: npm run build:release -- --win dir
|
||||
if: github.ref != 'refs/heads/main'
|
||||
env:
|
||||
DISABLE_INSPECT_FUSE: on
|
||||
|
||||
- run: yarn test-electron
|
||||
- run: npm run test-electron
|
||||
env:
|
||||
ARTIFACTS_DIR: artifacts/windows
|
||||
timeout-minutes: 5
|
||||
- run: yarn test-release
|
||||
- run: npm run test-release
|
||||
env:
|
||||
SIGNAL_ENV: production
|
||||
|
||||
- name: Upload artifacts on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
|
@ -264,29 +271,31 @@ jobs:
|
|||
working-directory: sticker-creator
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.17.1'
|
||||
- run: npm install -g yarn@1.22.10
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install Sticker Creator node_modules
|
||||
run: yarn install
|
||||
run: npm ci
|
||||
|
||||
- name: Build Sticker Creator
|
||||
run: yarn build
|
||||
run: npm run build
|
||||
|
||||
- name: Check Sticker Creator types
|
||||
run: yarn check:types
|
||||
run: npm run check:types
|
||||
|
||||
- name: Check Sticker Creator formatting
|
||||
run: yarn prettier:check
|
||||
run: npm run prettier:check
|
||||
|
||||
- name: Check Sticker Creator linting
|
||||
run: yarn lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run tests
|
||||
run: yarn test --run
|
||||
run: npm test -- --run
|
||||
|
||||
mock-tests:
|
||||
needs: lint
|
||||
|
@ -301,40 +310,40 @@ jobs:
|
|||
run: uname -a
|
||||
|
||||
- name: Clone Desktop repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.9.0'
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install global dependencies
|
||||
run: npm install -g yarn@1.22.10 npm@10.2.5
|
||||
run: npm install -g npm@10.2.5
|
||||
|
||||
- name: Install xvfb
|
||||
run: sudo apt-get install xvfb
|
||||
|
||||
- name: Cache Desktop node_modules
|
||||
id: cache-desktop-modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', 'patches/**') }}
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'patches/**') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
run: npm ci
|
||||
env:
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
|
||||
- name: Build typescript
|
||||
run: yarn generate
|
||||
run: npm run generate
|
||||
- name: Bundle
|
||||
run: yarn build:esbuild:prod
|
||||
run: npm run build:esbuild:prod
|
||||
|
||||
- name: Run mock server tests
|
||||
run: |
|
||||
set -o pipefail
|
||||
xvfb-run --auto-servernum yarn test-mock
|
||||
xvfb-run --auto-servernum npm run test-mock
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
@ -343,7 +352,7 @@ jobs:
|
|||
|
||||
- name: Upload mock server test logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: logs
|
||||
path: artifacts
|
||||
|
|
17
.github/workflows/danger.yml
vendored
17
.github/workflows/danger.yml
vendored
|
@ -10,23 +10,26 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # fetch all history
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.9.0'
|
||||
- run: npm install -g yarn@1.22.10 npm@10.2.5
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
- run: npm install -g npm@10.2.5
|
||||
- name: Cache danger node_modules
|
||||
id: cache-desktop-modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: danger/node_modules
|
||||
key: danger-${{ runner.os }}-${{ hashFiles('danger/package.json', 'danger/yarn.lock') }}
|
||||
key: danger-${{ runner.os }}-${{ hashFiles('danger/package.json', 'danger/package-lock.json') }}
|
||||
- name: Install danger node_modules
|
||||
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
|
||||
run: cd danger && yarn install --frozen-lockfile
|
||||
run: cd danger && npm ci
|
||||
- name: Run DangerJS
|
||||
run: yarn danger:ci
|
||||
run: npm run danger:ci
|
||||
env:
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
|
|
2
.github/workflows/notes.yml
vendored
2
.github/workflows/notes.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
repository: signalapp/Signal-Notes-Action-Private
|
||||
|
|
28
.github/workflows/release-notes.yml
vendored
Normal file
28
.github/workflows/release-notes.yml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Copyright 2024 Signal Messenger, LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
name: Release Notes
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request:
|
||||
types: [opened, labeled, unlabeled, closed]
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Update release notes issue
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
repository: signalapp/Signal-Release-Notes-Action-Private
|
||||
path: ./.github/actions/release-notes
|
||||
- name: Run action
|
||||
uses: ./.github/actions/release-notes
|
||||
with:
|
||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
25
.github/workflows/stories.yml
vendored
25
.github/workflows/stories.yml
vendored
|
@ -13,25 +13,20 @@ jobs:
|
|||
runs-on: ubuntu-latest-8-cores
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.9.0'
|
||||
cache: 'yarn'
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
- name: Install global dependencies
|
||||
run: npm install -g yarn@1.22.10 npm@10.2.5
|
||||
- name: Cache Desktop node_modules
|
||||
id: cache-desktop-modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', 'patches/**') }}
|
||||
run: npm install -g npm@10.2.5
|
||||
- name: Install Desktop node_modules
|
||||
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --prefer-offline
|
||||
run: npm ci
|
||||
env:
|
||||
CHILD_CONCURRENCY: 1
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
- run: yarn build:storybook
|
||||
- run: npm run build:storybook
|
||||
- run: npx playwright install chromium
|
||||
- run: yarn run-p --race test:storybook:serve test:storybook:test
|
||||
- run: ./node_modules/.bin/run-p --race test:storybook:serve test:storybook:test
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -4,11 +4,12 @@ node_modules_bkp
|
|||
coverage/*
|
||||
build/curve25519_compiled.js
|
||||
build/dns-fallback.json
|
||||
build/compact-locales
|
||||
stylesheets/*.css.map
|
||||
/dist
|
||||
.DS_Store
|
||||
config/local.json
|
||||
config/local-*.json
|
||||
config/local-*
|
||||
*.provisionprofile
|
||||
release/
|
||||
/dev-app-update.yml
|
||||
|
@ -26,6 +27,7 @@ js/components.js
|
|||
js/util_worker.js
|
||||
libtextsecure/components.js
|
||||
stylesheets/*.css
|
||||
!stylesheets/webrtc_internals.css
|
||||
/storybook-static/
|
||||
preload.bundle.*
|
||||
bundles/
|
||||
|
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
legacy-peer-deps=true
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
|||
20.9.0
|
||||
20.15.1
|
||||
|
|
|
@ -21,13 +21,18 @@ ts/util/lint/exceptions.json
|
|||
storybook-static
|
||||
build/locale-display-names.json
|
||||
build/country-display-names.json
|
||||
build/compact-locales/**/*.json
|
||||
release/**
|
||||
|
||||
# Third-party files
|
||||
node_modules/**
|
||||
danger/node_modules/**
|
||||
sticker-creator/node_modules/**
|
||||
components/**
|
||||
js/curve/**
|
||||
js/Mp3LameEncoder.min.js
|
||||
js/WebAudioRecorderMp3.js
|
||||
js/calling-tools/**
|
||||
|
||||
# Assets
|
||||
/images/
|
||||
|
|
|
@ -8,7 +8,9 @@ const config: StorybookConfig = {
|
|||
typescript: {
|
||||
reactDocgen: false,
|
||||
},
|
||||
|
||||
stories: ['../ts/components/**/*.stories.tsx'],
|
||||
|
||||
addons: [
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-actions',
|
||||
|
@ -17,15 +19,19 @@ const config: StorybookConfig = {
|
|||
'@storybook/addon-toolbars',
|
||||
'@storybook/addon-viewport',
|
||||
'@storybook/addon-jest',
|
||||
|
||||
// This must be imported last.
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-webpack5-compiler-swc',
|
||||
],
|
||||
|
||||
framework: '@storybook/react-webpack5',
|
||||
core: {},
|
||||
features: {
|
||||
storyStoreV7: true,
|
||||
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
|
||||
features: {},
|
||||
|
||||
staticDirs: [
|
||||
{ from: '../fonts', to: 'fonts' },
|
||||
{ from: '../images', to: 'images' },
|
||||
|
@ -35,6 +41,7 @@ const config: StorybookConfig = {
|
|||
to: 'node_modules/emoji-datasource-apple/img',
|
||||
},
|
||||
],
|
||||
|
||||
webpackFinal(config) {
|
||||
config.cache = {
|
||||
type: 'filesystem',
|
||||
|
@ -97,6 +104,8 @@ const config: StorybookConfig = {
|
|||
|
||||
return config;
|
||||
},
|
||||
|
||||
docs: {},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
@ -21,6 +21,9 @@ import {
|
|||
ScrollerLockContext,
|
||||
createScrollerLock,
|
||||
} from '../ts/hooks/useScrollLock';
|
||||
import { Environment, setEnvironment } from '../ts/environment.ts';
|
||||
|
||||
setEnvironment(Environment.Development, true);
|
||||
|
||||
const i18n = setupI18n('en', messages);
|
||||
|
||||
|
@ -80,6 +83,7 @@ const noop = () => {};
|
|||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper.events = {
|
||||
on: noop,
|
||||
off: noop,
|
||||
};
|
||||
|
||||
window.SignalContext = {
|
||||
|
@ -93,7 +97,6 @@ window.SignalContext = {
|
|||
unregisterForChange: noop,
|
||||
},
|
||||
|
||||
isTestOrMockEnvironment: () => false,
|
||||
nativeThemeListener: {
|
||||
getSystemTheme: () => 'light',
|
||||
subscribe: noop,
|
||||
|
@ -116,7 +119,6 @@ window.SignalContext = {
|
|||
|
||||
getHourCyclePreference: () => HourCyclePreference.UnknownPreference,
|
||||
getPreferredSystemLocales: () => ['en'],
|
||||
getResolvedMessagesLocaleDirection: () => 'ltr',
|
||||
getLocaleOverride: () => null,
|
||||
getLocaleDisplayNames: () => ({ en: { en: 'English' } }),
|
||||
};
|
||||
|
@ -133,6 +135,9 @@ const withGlobalTypesProvider = (Story, context) => {
|
|||
const mode = context.globals.mode;
|
||||
const direction = context.globals.direction ?? 'auto';
|
||||
|
||||
window.SignalContext.getResolvedMessagesLocaleDirection = () =>
|
||||
direction === 'auto' ? 'ltr' : direction;
|
||||
|
||||
// Adding it to the body as well so that we can cover modals and other
|
||||
// components that are rendered outside of this decorator container
|
||||
if (theme === 'light') {
|
||||
|
@ -193,3 +198,4 @@ export const parameters = {
|
|||
disabledRules: ['html-has-lang'],
|
||||
},
|
||||
};
|
||||
export const tags = [];
|
||||
|
|
2068
ACKNOWLEDGMENTS.md
2068
ACKNOWLEDGMENTS.md
File diff suppressed because it is too large
Load diff
|
@ -52,13 +52,12 @@ Install the [Xcode Command-Line Tools](http://osxdaily.com/2014/02/12/install-co
|
|||
Now, run these commands in your preferred terminal in a good directory for development:
|
||||
|
||||
```
|
||||
npm install --global yarn # Make sure you have have `yarn`
|
||||
git clone https://github.com/signalapp/Signal-Desktop.git
|
||||
cd Signal-Desktop
|
||||
yarn install --frozen-lockfile # Install and build dependencies (this will take a while)
|
||||
yarn generate # Generate final JS and CSS assets
|
||||
yarn test # A good idea to make sure tests run first
|
||||
yarn start # Start Signal!
|
||||
npm install # Install and build dependencies (this will take a while)
|
||||
npm run generate # Generate final JS and CSS assets
|
||||
npm test # A good idea to make sure tests run first
|
||||
npm start # Start Signal!
|
||||
```
|
||||
|
||||
You'll need to restart the application regularly to see your changes, as there
|
||||
|
@ -68,14 +67,53 @@ is no automatic restart mechanism. Alternatively, keep the developer tools open
|
|||
(Windows & Linux).
|
||||
|
||||
Also, note that the assets loaded by the application are not necessarily the same files
|
||||
you’re touching. You may not see your changes until you run `yarn generate` on the
|
||||
you’re touching. You may not see your changes until you run `npm run generate` on the
|
||||
command-line like you did during setup. You can make it easier on yourself by generating
|
||||
the latest built assets when you change a file. Run each of these in their own terminal
|
||||
instance while you make changes - they'll run until you stop them:
|
||||
|
||||
```
|
||||
yarn dev:transpile # recompiles when you change .ts files
|
||||
yarn dev:sass # recompiles when you change .scss files
|
||||
npm run dev:transpile # recompiles when you change .ts files
|
||||
npm run dev:sass # recompiles when you change .scss files
|
||||
```
|
||||
|
||||
#### Known issues
|
||||
|
||||
##### `yarn install` prints error 'Could not detect abi for version 30.0.6 and runtime electron'
|
||||
|
||||
`yarn install` may print an error like the following, but it can be ignored because the overall operation succeeds.
|
||||
|
||||
```
|
||||
$ ./node_modules/.bin/electron-builder install-app-deps
|
||||
|
||||
• electron-builder version=24.6.3
|
||||
• loaded configuration file=package.json ("build" field)
|
||||
• rebuilding native dependencies dependencies=@nodert-win10-rs4/windows.data.xml.dom@0.4.4, @nodert-win10-rs4/windows.ui.notifications@0.4.4, @signalapp/better-sqlite3@8.7.1, @signalapp/windows-dummy-keystroke@1.0.0, bufferutil@4.0.7, fs-xattr@0.3.0, mac-screen-capture-permissions@2.0.0, utf-8-validate@5.0.10
|
||||
platform=linux
|
||||
arch=x64
|
||||
• install prebuilt binary name=mac-screen-capture-permissions version=2.0.0 platform=linux arch=x64 napi=
|
||||
• build native dependency from sources name=mac-screen-capture-permissions
|
||||
version=2.0.0
|
||||
platform=linux
|
||||
arch=x64
|
||||
napi=
|
||||
reason=prebuild-install failed with error (run with env DEBUG=electron-builder to get more information)
|
||||
error=/home/ben/sauce/Signal-Desktop/node_modules/node-abi/index.js:30
|
||||
throw new Error('Could not detect abi for version ' + target + ' and runtime ' + runtime + '. Updating "node-abi" might help solve this issue if it is a new release of ' + runtime)
|
||||
^
|
||||
|
||||
Error: Could not detect abi for version 30.0.6 and runtime electron. Updating "node-abi" might help solve this issue if it is a new release of electron
|
||||
at getAbi (/home/ben/sauce/Signal-Desktop/node_modules/node-abi/index.js:30:9)
|
||||
at module.exports (/home/ben/sauce/Signal-Desktop/node_modules/prebuild-install/rc.js:53:57)
|
||||
at Object.<anonymous> (/home/ben/sauce/Signal-Desktop/node_modules/prebuild-install/bin.js:8:25)
|
||||
at Module._compile (node:internal/modules/cjs/loader:1376:14)
|
||||
at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
|
||||
at Module.load (node:internal/modules/cjs/loader:1207:32)
|
||||
at Module._load (node:internal/modules/cjs/loader:1023:12)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)
|
||||
at node:internal/main/run_main_module:28:49
|
||||
|
||||
Node.js v20.11.1
|
||||
```
|
||||
|
||||
### webpack
|
||||
|
@ -85,7 +123,7 @@ You can run a development server for these parts of the app with the
|
|||
following command:
|
||||
|
||||
```
|
||||
yarn dev
|
||||
npm run dev
|
||||
```
|
||||
|
||||
In order for the app to make requests to the development server you must set
|
||||
|
@ -93,7 +131,7 @@ the `SIGNAL_ENABLE_HTTP` environment variable to a truthy value. On Linux and
|
|||
macOS, that simply looks like this:
|
||||
|
||||
```
|
||||
SIGNAL_ENABLE_HTTP=1 yarn start
|
||||
SIGNAL_ENABLE_HTTP=1 npm start
|
||||
```
|
||||
|
||||
## Setting up standalone
|
||||
|
@ -158,7 +196,7 @@ For example, to create an 'alice' profile, put a file called `local-alice.json`
|
|||
Then you can start up the application a little differently to load the profile:
|
||||
|
||||
```
|
||||
NODE_APP_INSTANCE=alice yarn run start
|
||||
NODE_APP_INSTANCE=alice npm start
|
||||
```
|
||||
|
||||
This changes the `userData` directory from `%appData%/Signal` to `%appData%/Signal-aliceProfile`.
|
||||
|
@ -174,15 +212,15 @@ Please write tests! Our testing framework is
|
|||
[mocha](http://mochajs.org/) and our assertion library is
|
||||
[chai](http://chaijs.com/api/assert/).
|
||||
|
||||
The easiest way to run all tests at once is `yarn test`, which will run them on the
|
||||
The easiest way to run all tests at once is `npm test`, which will run them on the
|
||||
command line. You can run the client-side tests in an interactive session with
|
||||
`NODE_ENV=test yarn run start`.
|
||||
`NODE_ENV=test npm start`.
|
||||
|
||||
## Pull requests
|
||||
|
||||
So you wanna make a pull request? Please observe the following guidelines.
|
||||
|
||||
- First, make sure that your `yarn ready` run passes - it's very similar to what our
|
||||
- First, make sure that your `npm run ready` run passes - it's very similar to what our
|
||||
Continuous Integration servers do to test the app.
|
||||
- Please do not submit pull requests for translation fixes.
|
||||
- Never use plain strings right in the source code - pull them from `messages.json`!
|
||||
|
@ -261,8 +299,27 @@ will go to your new development desktop app instead of your phone.
|
|||
To test changes to the build system, build a release using
|
||||
|
||||
```
|
||||
yarn generate
|
||||
yarn build
|
||||
npm run generate
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then, run the tests using `yarn test-release`.
|
||||
Then, run the tests using `npm run test-release`.
|
||||
|
||||
### Testing MacOS builds
|
||||
|
||||
macOS requires apps to be code signed with an Apple certificate. To test development builds
|
||||
you can ad-hoc sign the packaged app which will let you run it locally.
|
||||
|
||||
1. In `package.json` remove the macOS signing script: `"sign": "./ts/scripts/sign-macos.js",`
|
||||
2. Build the app and ad-hoc sign the app bundle:
|
||||
|
||||
```
|
||||
npm run generate
|
||||
npm run build
|
||||
cd release
|
||||
# Pick the desired app bundle: mac, mac-arm64, or mac-universal
|
||||
cd mac-arm64
|
||||
codesign --force --deep --sign - Signal.app
|
||||
```
|
||||
|
||||
3. Now you can run the app locally.
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
113
app/EmojiService.ts
Normal file
113
app/EmojiService.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import * as z from 'zod';
|
||||
import { protocol } from 'electron';
|
||||
import LRU from 'lru-cache';
|
||||
|
||||
import type { OptionalResourceService } from './OptionalResourceService';
|
||||
import { SignalService as Proto } from '../ts/protobuf';
|
||||
|
||||
const MANIFEST_PATH = join(__dirname, '..', 'build', 'jumbomoji.json');
|
||||
|
||||
const manifestSchema = z.record(z.string(), z.string().array());
|
||||
|
||||
function utf16ToEmoji(utf16: string): string {
|
||||
const codePoints = new Array<number>();
|
||||
const buf = Buffer.from(utf16, 'hex');
|
||||
for (let i = 0; i < buf.length; i += 2) {
|
||||
codePoints.push(buf.readUint16BE(i));
|
||||
}
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
||||
export type ManifestType = z.infer<typeof manifestSchema>;
|
||||
|
||||
type EmojiEntryType = Readonly<{
|
||||
utf16: string;
|
||||
sheet: string;
|
||||
}>;
|
||||
|
||||
type SheetCacheEntry = Map<string, Uint8Array>;
|
||||
|
||||
export class EmojiService {
|
||||
private readonly emojiMap = new Map<string, EmojiEntryType>();
|
||||
|
||||
private readonly sheetCache = new LRU<string, SheetCacheEntry>({
|
||||
// Each sheet is roughly 500kb
|
||||
max: 10,
|
||||
});
|
||||
|
||||
private constructor(
|
||||
private readonly resourceService: OptionalResourceService,
|
||||
manifest: ManifestType
|
||||
) {
|
||||
protocol.handle('emoji', async req => {
|
||||
const url = new URL(req.url);
|
||||
const emoji = url.searchParams.get('emoji');
|
||||
if (!emoji) {
|
||||
return new Response('invalid', { status: 400 });
|
||||
}
|
||||
|
||||
return this.fetch(emoji);
|
||||
});
|
||||
|
||||
for (const [sheet, emojiList] of Object.entries(manifest)) {
|
||||
for (const utf16 of emojiList) {
|
||||
this.emojiMap.set(utf16ToEmoji(utf16), { sheet, utf16 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async create(
|
||||
resourceService: OptionalResourceService
|
||||
): Promise<EmojiService> {
|
||||
const json = await readFile(MANIFEST_PATH, 'utf8');
|
||||
const manifest = manifestSchema.parse(JSON.parse(json));
|
||||
return new EmojiService(resourceService, manifest);
|
||||
}
|
||||
|
||||
private async fetch(emoji: string): Promise<Response> {
|
||||
const entry = this.emojiMap.get(emoji);
|
||||
if (!entry) {
|
||||
return new Response('entry not found', { status: 404 });
|
||||
}
|
||||
|
||||
const { sheet, utf16 } = entry;
|
||||
|
||||
let imageMap = this.sheetCache.get(sheet);
|
||||
if (!imageMap) {
|
||||
const proto = await this.resourceService.getData(
|
||||
`emoji-sheet-${sheet}.proto`
|
||||
);
|
||||
if (!proto) {
|
||||
return new Response('resource not found', { status: 404 });
|
||||
}
|
||||
|
||||
const pack = Proto.JumbomojiPack.decode(proto);
|
||||
|
||||
imageMap = new Map(
|
||||
pack.items.map(({ name, image }) => [
|
||||
name ?? '',
|
||||
image || new Uint8Array(0),
|
||||
])
|
||||
);
|
||||
this.sheetCache.set(sheet, imageMap);
|
||||
}
|
||||
|
||||
const image = imageMap.get(utf16);
|
||||
if (!image) {
|
||||
return new Response('image not found', { status: 404 });
|
||||
}
|
||||
|
||||
return new Response(image, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'image/webp',
|
||||
'cache-control': 'public, max-age=2592000, immutable',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
188
app/OptionalResourceService.ts
Normal file
188
app/OptionalResourceService.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { join, dirname } from 'node:path';
|
||||
import { mkdir, readFile, readdir, writeFile, unlink } from 'node:fs/promises';
|
||||
import { createHash, timingSafeEqual } from 'node:crypto';
|
||||
import { ipcMain } from 'electron';
|
||||
import LRU from 'lru-cache';
|
||||
import got from 'got';
|
||||
import PQueue from 'p-queue';
|
||||
|
||||
import type {
|
||||
OptionalResourceType,
|
||||
OptionalResourcesDictType,
|
||||
} from '../ts/types/OptionalResource';
|
||||
import { OptionalResourcesDictSchema } from '../ts/types/OptionalResource';
|
||||
import * as log from '../ts/logging/log';
|
||||
import { getGotOptions } from '../ts/updater/got';
|
||||
import { drop } from '../ts/util/drop';
|
||||
|
||||
const RESOURCES_DICT_PATH = join(
|
||||
__dirname,
|
||||
'..',
|
||||
'build',
|
||||
'optional-resources.json'
|
||||
);
|
||||
|
||||
const MAX_CACHE_SIZE = 50 * 1024 * 1024;
|
||||
|
||||
export class OptionalResourceService {
|
||||
private maybeDeclaration: OptionalResourcesDictType | undefined;
|
||||
|
||||
private readonly cache = new LRU<string, Buffer>({
|
||||
max: MAX_CACHE_SIZE,
|
||||
|
||||
length: buf => buf.length,
|
||||
});
|
||||
|
||||
private readonly fileQueues = new Map<string, PQueue>();
|
||||
|
||||
private constructor(private readonly resourcesDir: string) {
|
||||
ipcMain.handle('OptionalResourceService:getData', (_event, name) =>
|
||||
this.getData(name)
|
||||
);
|
||||
|
||||
drop(this.lazyInit());
|
||||
}
|
||||
|
||||
public static create(resourcesDir: string): OptionalResourceService {
|
||||
return new OptionalResourceService(resourcesDir);
|
||||
}
|
||||
|
||||
public async getData(name: string): Promise<Buffer | undefined> {
|
||||
await this.lazyInit();
|
||||
|
||||
const decl = this.declaration[name];
|
||||
if (!decl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const inMemory = this.cache.get(name);
|
||||
if (inMemory) {
|
||||
return inMemory;
|
||||
}
|
||||
|
||||
const filePath = join(this.resourcesDir, name);
|
||||
return this.queueFileWork(filePath, async () => {
|
||||
try {
|
||||
const onDisk = await readFile(filePath);
|
||||
const digest = createHash('sha512').update(onDisk).digest();
|
||||
|
||||
// Same digest and size
|
||||
if (
|
||||
timingSafeEqual(digest, Buffer.from(decl.digest, 'base64')) &&
|
||||
onDisk.length === decl.size
|
||||
) {
|
||||
log.warn(`OptionalResourceService: loaded ${name} from disk`);
|
||||
this.cache.set(name, onDisk);
|
||||
return onDisk;
|
||||
}
|
||||
|
||||
log.warn(`OptionalResourceService: ${name} is no longer valid on disk`);
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// We get here if file doesn't exist or if its digest/size is different
|
||||
try {
|
||||
await unlink(filePath);
|
||||
} catch {
|
||||
// Just do our best effort and move forward
|
||||
}
|
||||
|
||||
return this.fetch(name, decl, filePath);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
||||
private async lazyInit(): Promise<void> {
|
||||
if (this.maybeDeclaration !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const json = JSON.parse(await readFile(RESOURCES_DICT_PATH, 'utf8'));
|
||||
this.maybeDeclaration = OptionalResourcesDictSchema.parse(json);
|
||||
|
||||
// Clean unknown resources
|
||||
let subPaths: Array<string>;
|
||||
try {
|
||||
subPaths = await readdir(this.resourcesDir);
|
||||
} catch (error) {
|
||||
// Directory wasn't created yet
|
||||
if (error.code === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
subPaths.map(async subPath => {
|
||||
if (this.declaration[subPath]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = join(this.resourcesDir, subPath);
|
||||
|
||||
try {
|
||||
await unlink(fullPath);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`OptionalResourceService: failed to cleanup ${subPath}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private get declaration(): OptionalResourcesDictType {
|
||||
if (this.maybeDeclaration === undefined) {
|
||||
throw new Error('optional-resources.json not loaded yet');
|
||||
}
|
||||
return this.maybeDeclaration;
|
||||
}
|
||||
|
||||
private async queueFileWork<R>(
|
||||
filePath: string,
|
||||
body: () => Promise<R>
|
||||
): Promise<R> {
|
||||
let queue = this.fileQueues.get(filePath);
|
||||
if (!queue) {
|
||||
queue = new PQueue({ concurrency: 1 });
|
||||
this.fileQueues.set(filePath, queue);
|
||||
}
|
||||
try {
|
||||
return await queue.add(body);
|
||||
} finally {
|
||||
if (queue.size === 0) {
|
||||
this.fileQueues.delete(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async fetch(
|
||||
name: string,
|
||||
decl: OptionalResourceType,
|
||||
destPath: string
|
||||
): Promise<Buffer> {
|
||||
const result = await got(decl.url, await getGotOptions()).buffer();
|
||||
|
||||
this.cache.set(name, result);
|
||||
|
||||
try {
|
||||
await mkdir(dirname(destPath), { recursive: true });
|
||||
await writeFile(destPath, result);
|
||||
} catch (error) {
|
||||
log.error('OptionalResourceService: failed to save file', error);
|
||||
// Still return the data that we just fetched
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import {
|
|||
SystemTraySetting,
|
||||
} from '../ts/types/SystemTraySetting';
|
||||
import { isSystemTraySupported } from '../ts/types/Settings';
|
||||
import type { MainSQL } from '../ts/sql/main';
|
||||
import type { ConfigType } from './base_config';
|
||||
|
||||
/**
|
||||
|
@ -21,10 +20,8 @@ export class SystemTraySettingCache {
|
|||
private getPromise: undefined | Promise<SystemTraySetting>;
|
||||
|
||||
constructor(
|
||||
private readonly sql: Pick<MainSQL, 'sqlCall'>,
|
||||
private readonly ephemeralConfig: Pick<ConfigType, 'get' | 'set'>,
|
||||
private readonly argv: Array<string>,
|
||||
private readonly appVersion: string
|
||||
private readonly argv: Array<string>
|
||||
) {}
|
||||
|
||||
async get(): Promise<SystemTraySetting> {
|
||||
|
@ -55,16 +52,12 @@ export class SystemTraySettingCache {
|
|||
log.info(
|
||||
`getSystemTraySetting saw --use-tray-icon flag. Returning ${result}`
|
||||
);
|
||||
} else if (isSystemTraySupported(OS, this.appVersion)) {
|
||||
const fastValue = this.ephemeralConfig.get('system-tray-setting');
|
||||
if (fastValue !== undefined) {
|
||||
log.info('getSystemTraySetting got fast value', fastValue);
|
||||
} else if (isSystemTraySupported(OS)) {
|
||||
const value = this.ephemeralConfig.get('system-tray-setting');
|
||||
if (value !== undefined) {
|
||||
log.info('getSystemTraySetting got value', value);
|
||||
}
|
||||
|
||||
const value =
|
||||
fastValue ??
|
||||
(await this.sql.sqlCall('getItemById', 'system-tray-setting'))?.value;
|
||||
|
||||
if (value !== undefined) {
|
||||
result = parseSystemTraySetting(value);
|
||||
log.info(`getSystemTraySetting returning ${result}`);
|
||||
|
@ -73,7 +66,7 @@ export class SystemTraySettingCache {
|
|||
log.info(`getSystemTraySetting got no value, returning ${result}`);
|
||||
}
|
||||
|
||||
if (result !== fastValue) {
|
||||
if (result !== value) {
|
||||
this.ephemeralConfig.set('system-tray-setting', result);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
import { ipcMain, protocol } from 'electron';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { join, normalize } from 'node:path';
|
||||
import { Readable, PassThrough } from 'node:stream';
|
||||
import z from 'zod';
|
||||
import * as rimraf from 'rimraf';
|
||||
import { RangeFinder, DefaultStorage } from '@indutny/range-finder';
|
||||
import {
|
||||
getAllAttachments,
|
||||
getAvatarsPath,
|
||||
getPath,
|
||||
getStickersPath,
|
||||
getTempPath,
|
||||
|
@ -20,6 +26,13 @@ import type { MainSQL } from '../ts/sql/main';
|
|||
import type { MessageAttachmentsCursorType } from '../ts/sql/Interface';
|
||||
import * as Errors from '../ts/types/errors';
|
||||
import { sleep } from '../ts/util/sleep';
|
||||
import { isPathInside } from '../ts/util/isPathInside';
|
||||
import { missingCaseError } from '../ts/util/missingCaseError';
|
||||
import { safeParseInteger } from '../ts/util/numbers';
|
||||
import { SECOND } from '../ts/util/durations';
|
||||
import { drop } from '../ts/util/drop';
|
||||
import { strictAssert } from '../ts/util/assert';
|
||||
import { decryptAttachmentV2ToSink } from '../ts/AttachmentCrypto';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
|
@ -31,6 +44,89 @@ const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
|||
|
||||
const INTERACTIVITY_DELAY = 50;
|
||||
|
||||
type RangeFinderContextType = Readonly<
|
||||
| {
|
||||
type: 'ciphertext';
|
||||
path: string;
|
||||
keysBase64: string;
|
||||
size: number;
|
||||
}
|
||||
| {
|
||||
type: 'plaintext';
|
||||
path: string;
|
||||
}
|
||||
>;
|
||||
|
||||
async function safeDecryptToSink(
|
||||
...args: Parameters<typeof decryptAttachmentV2ToSink>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await decryptAttachmentV2ToSink(...args);
|
||||
} catch (error) {
|
||||
// These errors happen when canceling fetch from `attachment://` urls,
|
||||
// ignore them to avoid noise in the logs.
|
||||
if (
|
||||
error.name === 'AbortError' ||
|
||||
error.code === 'ERR_STREAM_PREMATURE_CLOSE'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(
|
||||
'handleAttachmentRequest: decryption error',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const storage = new DefaultStorage<RangeFinderContextType>(
|
||||
ctx => {
|
||||
if (ctx.type === 'plaintext') {
|
||||
return createReadStream(ctx.path);
|
||||
}
|
||||
|
||||
if (ctx.type === 'ciphertext') {
|
||||
const options = {
|
||||
ciphertextPath: ctx.path,
|
||||
idForLogging: 'attachment_channel',
|
||||
keysBase64: ctx.keysBase64,
|
||||
type: 'local' as const,
|
||||
size: ctx.size,
|
||||
};
|
||||
|
||||
const plaintext = new PassThrough();
|
||||
drop(safeDecryptToSink(options, plaintext));
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
throw missingCaseError(ctx);
|
||||
},
|
||||
{
|
||||
maxSize: 10,
|
||||
ttl: SECOND,
|
||||
cacheKey: ctx => {
|
||||
if (ctx.type === 'ciphertext') {
|
||||
return `${ctx.type}:${ctx.path}:${ctx.size}:${ctx.keysBase64}`;
|
||||
}
|
||||
if (ctx.type === 'plaintext') {
|
||||
return `${ctx.type}:${ctx.path}`;
|
||||
}
|
||||
throw missingCaseError(ctx);
|
||||
},
|
||||
}
|
||||
);
|
||||
const rangeFinder = new RangeFinder<RangeFinderContextType>(storage, {
|
||||
noActiveReuse: true,
|
||||
});
|
||||
|
||||
const dispositionSchema = z.enum([
|
||||
'attachment',
|
||||
'temporary',
|
||||
'draft',
|
||||
'sticker',
|
||||
'avatarData',
|
||||
]);
|
||||
|
||||
type DeleteOrphanedAttachmentsOptionsType = Readonly<{
|
||||
orphanedAttachments: Set<string>;
|
||||
sql: MainSQL;
|
||||
|
@ -48,11 +144,11 @@ async function cleanupOrphanedAttachments({
|
|||
}: CleanupOrphanedAttachmentsOptionsType): Promise<void> {
|
||||
await deleteAllBadges({
|
||||
userDataPath,
|
||||
pathsToKeep: await sql.sqlCall('getAllBadgeImageFileLocalPaths'),
|
||||
pathsToKeep: await sql.sqlRead('getAllBadgeImageFileLocalPaths'),
|
||||
});
|
||||
|
||||
const allStickers = await getAllStickers(userDataPath);
|
||||
const orphanedStickers = await sql.sqlCall(
|
||||
const orphanedStickers = await sql.sqlWrite(
|
||||
'removeKnownStickers',
|
||||
allStickers
|
||||
);
|
||||
|
@ -62,7 +158,7 @@ async function cleanupOrphanedAttachments({
|
|||
});
|
||||
|
||||
const allDraftAttachments = await getAllDraftAttachments(userDataPath);
|
||||
const orphanedDraftAttachments = await sql.sqlCall(
|
||||
const orphanedDraftAttachments = await sql.sqlWrite(
|
||||
'removeKnownDraftAttachments',
|
||||
allDraftAttachments
|
||||
);
|
||||
|
@ -80,7 +176,7 @@ async function cleanupOrphanedAttachments({
|
|||
);
|
||||
|
||||
{
|
||||
const attachments: ReadonlyArray<string> = await sql.sqlCall(
|
||||
const attachments: ReadonlyArray<string> = await sql.sqlRead(
|
||||
'getKnownConversationAttachments'
|
||||
);
|
||||
|
||||
|
@ -122,7 +218,7 @@ function deleteOrphanedAttachments({
|
|||
let attachments: ReadonlyArray<string>;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
({ attachments, cursor } = await sql.sqlCall(
|
||||
({ attachments, cursor } = await sql.sqlRead(
|
||||
'getKnownMessageAttachments',
|
||||
cursor
|
||||
));
|
||||
|
@ -146,7 +242,7 @@ function deleteOrphanedAttachments({
|
|||
} while (cursor !== undefined && !cursor.done);
|
||||
} finally {
|
||||
if (cursor !== undefined) {
|
||||
await sql.sqlCall('finishGetKnownMessageAttachments', cursor);
|
||||
await sql.sqlRead('finishGetKnownMessageAttachments', cursor);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,6 +277,12 @@ function deleteOrphanedAttachments({
|
|||
void runSafe();
|
||||
}
|
||||
|
||||
let attachmentsDir: string | undefined;
|
||||
let stickersDir: string | undefined;
|
||||
let tempDir: string | undefined;
|
||||
let draftDir: string | undefined;
|
||||
let avatarDataDir: string | undefined;
|
||||
|
||||
export function initialize({
|
||||
configDir,
|
||||
sql,
|
||||
|
@ -193,15 +295,28 @@ export function initialize({
|
|||
}
|
||||
initialized = true;
|
||||
|
||||
const attachmentsDir = getPath(configDir);
|
||||
const stickersDir = getStickersPath(configDir);
|
||||
const tempDir = getTempPath(configDir);
|
||||
const draftDir = getDraftPath(configDir);
|
||||
attachmentsDir = getPath(configDir);
|
||||
stickersDir = getStickersPath(configDir);
|
||||
tempDir = getTempPath(configDir);
|
||||
draftDir = getDraftPath(configDir);
|
||||
avatarDataDir = getAvatarsPath(configDir);
|
||||
|
||||
ipcMain.handle(ERASE_TEMP_KEY, () => rimraf.sync(tempDir));
|
||||
ipcMain.handle(ERASE_ATTACHMENTS_KEY, () => rimraf.sync(attachmentsDir));
|
||||
ipcMain.handle(ERASE_STICKERS_KEY, () => rimraf.sync(stickersDir));
|
||||
ipcMain.handle(ERASE_DRAFTS_KEY, () => rimraf.sync(draftDir));
|
||||
ipcMain.handle(ERASE_TEMP_KEY, () => {
|
||||
strictAssert(tempDir != null, 'not initialized');
|
||||
rimraf.sync(tempDir);
|
||||
});
|
||||
ipcMain.handle(ERASE_ATTACHMENTS_KEY, () => {
|
||||
strictAssert(attachmentsDir != null, 'not initialized');
|
||||
rimraf.sync(attachmentsDir);
|
||||
});
|
||||
ipcMain.handle(ERASE_STICKERS_KEY, () => {
|
||||
strictAssert(stickersDir != null, 'not initialized');
|
||||
rimraf.sync(stickersDir);
|
||||
});
|
||||
ipcMain.handle(ERASE_DRAFTS_KEY, () => {
|
||||
strictAssert(draftDir != null, 'not initialized');
|
||||
rimraf.sync(draftDir);
|
||||
});
|
||||
|
||||
ipcMain.handle(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async () => {
|
||||
const start = Date.now();
|
||||
|
@ -209,4 +324,172 @@ export function initialize({
|
|||
const duration = Date.now() - start;
|
||||
console.log(`cleanupOrphanedAttachments: took ${duration}ms`);
|
||||
});
|
||||
|
||||
protocol.handle('attachment', handleAttachmentRequest);
|
||||
}
|
||||
|
||||
export async function handleAttachmentRequest(req: Request): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
if (url.host !== 'v1' && url.host !== 'v2') {
|
||||
return new Response('Unknown host', { status: 404 });
|
||||
}
|
||||
|
||||
// Disposition
|
||||
let disposition: z.infer<typeof dispositionSchema> = 'attachment';
|
||||
const dispositionParam = url.searchParams.get('disposition');
|
||||
if (dispositionParam != null) {
|
||||
disposition = dispositionSchema.parse(dispositionParam);
|
||||
}
|
||||
|
||||
strictAssert(attachmentsDir != null, 'not initialized');
|
||||
strictAssert(tempDir != null, 'not initialized');
|
||||
strictAssert(draftDir != null, 'not initialized');
|
||||
strictAssert(stickersDir != null, 'not initialized');
|
||||
strictAssert(avatarDataDir != null, 'not initialized');
|
||||
|
||||
let parentDir: string;
|
||||
switch (disposition) {
|
||||
case 'attachment':
|
||||
parentDir = attachmentsDir;
|
||||
break;
|
||||
case 'temporary':
|
||||
parentDir = tempDir;
|
||||
break;
|
||||
case 'draft':
|
||||
parentDir = draftDir;
|
||||
break;
|
||||
case 'sticker':
|
||||
parentDir = stickersDir;
|
||||
break;
|
||||
case 'avatarData':
|
||||
parentDir = avatarDataDir;
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(disposition);
|
||||
}
|
||||
|
||||
// Remove first slash
|
||||
const path = normalize(
|
||||
join(parentDir, ...url.pathname.slice(1).split(/\//g))
|
||||
);
|
||||
if (!isPathInside(path, parentDir)) {
|
||||
return new Response('Access denied', { status: 401 });
|
||||
}
|
||||
|
||||
// Get attachment size to trim the padding
|
||||
const sizeParam = url.searchParams.get('size');
|
||||
let maybeSize: number | undefined;
|
||||
if (sizeParam != null) {
|
||||
const intValue = safeParseInteger(sizeParam);
|
||||
if (intValue != null) {
|
||||
maybeSize = intValue;
|
||||
}
|
||||
}
|
||||
|
||||
let context: RangeFinderContextType;
|
||||
|
||||
// Legacy plaintext attachments
|
||||
if (url.host === 'v1') {
|
||||
context = {
|
||||
type: 'plaintext',
|
||||
path,
|
||||
};
|
||||
} else {
|
||||
// Encrypted attachments
|
||||
|
||||
// Get AES+MAC key
|
||||
const maybeKeysBase64 = url.searchParams.get('key');
|
||||
if (maybeKeysBase64 == null) {
|
||||
return new Response('Missing key', { status: 400 });
|
||||
}
|
||||
|
||||
// Size is required for trimming padding
|
||||
if (maybeSize == null) {
|
||||
return new Response('Missing size', { status: 400 });
|
||||
}
|
||||
|
||||
context = {
|
||||
type: 'ciphertext',
|
||||
path,
|
||||
keysBase64: maybeKeysBase64,
|
||||
size: maybeSize,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return handleRangeRequest({
|
||||
request: req,
|
||||
size: maybeSize,
|
||||
context,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('handleAttachmentRequest: error', Errors.toLogFormat(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
type HandleRangeRequestOptionsType = Readonly<{
|
||||
request: Request;
|
||||
size: number | undefined;
|
||||
context: RangeFinderContextType;
|
||||
}>;
|
||||
|
||||
function handleRangeRequest({
|
||||
request,
|
||||
size,
|
||||
context,
|
||||
}: HandleRangeRequestOptionsType): Response {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Get content-type
|
||||
const contentType = url.searchParams.get('contentType');
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'cache-control': 'no-cache, no-store',
|
||||
'content-type': contentType || 'application/octet-stream',
|
||||
};
|
||||
|
||||
if (size != null) {
|
||||
headers['content-length'] = size.toString();
|
||||
}
|
||||
|
||||
const create200Response = (): Response => {
|
||||
const plaintext = rangeFinder.get(0, context);
|
||||
return new Response(Readable.toWeb(plaintext) as ReadableStream<Buffer>, {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
|
||||
const range = request.headers.get('range');
|
||||
if (range == null) {
|
||||
return create200Response();
|
||||
}
|
||||
|
||||
// Chromium only sends open-ended ranges: "start-"
|
||||
const match = range.match(/^bytes=(\d+)-$/);
|
||||
if (match == null) {
|
||||
console.error(`attachment_channel: invalid range header: ${range}`);
|
||||
return create200Response();
|
||||
}
|
||||
|
||||
const startParam = safeParseInteger(match[1]);
|
||||
if (startParam == null) {
|
||||
console.error(`attachment_channel: invalid range header: ${range}`);
|
||||
return create200Response();
|
||||
}
|
||||
|
||||
const start = Math.min(startParam, size || Infinity);
|
||||
|
||||
headers['content-range'] = `bytes ${start}-/${size ?? '*'}`;
|
||||
|
||||
if (size !== undefined) {
|
||||
headers['content-length'] = (size - start).toString();
|
||||
}
|
||||
|
||||
const stream = rangeFinder.get(start, context);
|
||||
return new Response(Readable.toWeb(stream) as ReadableStream<Buffer>, {
|
||||
status: 206,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { PassThrough } from 'node:stream';
|
||||
import { join, relative, normalize } from 'path';
|
||||
import fastGlob from 'fast-glob';
|
||||
import fse from 'fs-extra';
|
||||
import { map, isString } from 'lodash';
|
||||
import normalizePath from 'normalize-path';
|
||||
import { isPathInside } from '../ts/util/isPathInside';
|
||||
import {
|
||||
generateKeys,
|
||||
decryptAttachmentV2ToSink,
|
||||
encryptAttachmentV2ToDisk,
|
||||
} from '../ts/AttachmentCrypto';
|
||||
import type { LocalAttachmentV2Type } from '../ts/types/Attachment';
|
||||
|
||||
const PATH = 'attachments.noindex';
|
||||
const AVATAR_PATH = 'avatars.noindex';
|
||||
|
@ -190,3 +197,57 @@ export const deleteAllDraftAttachments = async ({
|
|||
|
||||
console.log(`deleteAllDraftAttachments: deleted ${attachments.length} files`);
|
||||
};
|
||||
|
||||
export const readAndDecryptDataFromDisk = async ({
|
||||
absolutePath,
|
||||
keysBase64,
|
||||
size,
|
||||
}: {
|
||||
absolutePath: string;
|
||||
keysBase64: string;
|
||||
size: number;
|
||||
}): Promise<Uint8Array> => {
|
||||
const sink = new PassThrough();
|
||||
|
||||
const chunks = new Array<Buffer>();
|
||||
|
||||
sink.on('data', chunk => chunks.push(chunk));
|
||||
sink.resume();
|
||||
|
||||
await decryptAttachmentV2ToSink(
|
||||
{
|
||||
ciphertextPath: absolutePath,
|
||||
idForLogging: 'attachments/readAndDecryptDataFromDisk',
|
||||
keysBase64,
|
||||
size,
|
||||
type: 'local',
|
||||
},
|
||||
sink
|
||||
);
|
||||
|
||||
return Buffer.concat(chunks);
|
||||
};
|
||||
|
||||
export const writeNewAttachmentData = async ({
|
||||
data,
|
||||
getAbsoluteAttachmentPath,
|
||||
}: {
|
||||
data: Uint8Array;
|
||||
getAbsoluteAttachmentPath: (relativePath: string) => string;
|
||||
}): Promise<LocalAttachmentV2Type> => {
|
||||
const keys = generateKeys();
|
||||
|
||||
const { plaintextHash, path } = await encryptAttachmentV2ToDisk({
|
||||
plaintext: { data },
|
||||
getAbsoluteAttachmentPath,
|
||||
keys,
|
||||
});
|
||||
|
||||
return {
|
||||
version: 2,
|
||||
plaintextHash,
|
||||
size: data.byteLength,
|
||||
path,
|
||||
localKey: Buffer.from(keys).toString('base64'),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
|
||||
import { readFileSync, unlinkSync } from 'fs';
|
||||
import { sync as writeFileSync } from 'write-file-atomic';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { set } from 'lodash/fp';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2017 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { join } from 'path';
|
||||
import { join, basename } from 'path';
|
||||
import { app } from 'electron';
|
||||
|
||||
import type { IConfig } from 'config';
|
||||
|
@ -15,9 +15,12 @@ import {
|
|||
|
||||
// In production mode, NODE_ENV cannot be customized by the user
|
||||
if (app.isPackaged) {
|
||||
setEnvironment(Environment.Production);
|
||||
setEnvironment(Environment.Production, false);
|
||||
} else {
|
||||
setEnvironment(parseEnvironment(process.env.NODE_ENV || 'development'));
|
||||
setEnvironment(
|
||||
parseEnvironment(process.env.NODE_ENV || 'development'),
|
||||
Boolean(process.env.MOCK_TEST)
|
||||
);
|
||||
}
|
||||
|
||||
// Set environment vars to configure node-config before requiring it
|
||||
|
@ -44,6 +47,12 @@ if (getEnvironment() === Environment.Production) {
|
|||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const config: IConfig = require('config');
|
||||
|
||||
if (getEnvironment() !== Environment.Production) {
|
||||
config.util.getConfigSources().forEach(source => {
|
||||
console.log(`config: Using config source ${basename(source.name)}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Log resulting env vars in use by config
|
||||
[
|
||||
'NODE_ENV',
|
||||
|
|
|
@ -6,7 +6,8 @@ import { readFileSync } from 'fs';
|
|||
import { merge } from 'lodash';
|
||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||
import { z } from 'zod';
|
||||
import { setupI18n } from '../ts/util/setupI18n';
|
||||
import { setupI18n } from '../ts/util/setupI18nMain';
|
||||
import { shouldNeverBeCalled } from '../ts/util/shouldNeverBeCalled';
|
||||
|
||||
import type { LoggerType } from '../ts/types/Logging';
|
||||
import type { HourCyclePreference, LocaleMessagesType } from '../ts/types/I18N';
|
||||
|
@ -142,7 +143,9 @@ export function load({
|
|||
|
||||
// We start with english, then overwrite that with anything present in locale
|
||||
const finalMessages = merge(englishMessages, matchedLocaleMessages);
|
||||
const i18n = setupI18n(matchedLocale, finalMessages);
|
||||
const i18n = setupI18n(matchedLocale, finalMessages, {
|
||||
renderEmojify: shouldNeverBeCalled,
|
||||
});
|
||||
const direction =
|
||||
localeDirectionTestingOverride ?? getLocaleDirection(matchedLocale, logger);
|
||||
logger.info(`locale: Text info direction for ${matchedLocale}: ${direction}`);
|
||||
|
|
610
app/main.ts
610
app/main.ts
|
@ -28,6 +28,8 @@ import {
|
|||
shell,
|
||||
systemPreferences,
|
||||
Notification,
|
||||
safeStorage,
|
||||
protocol as electronProtocol,
|
||||
} from 'electron';
|
||||
import type { MenuItemConstructorOptions, Settings } from 'electron';
|
||||
import { z } from 'zod';
|
||||
|
@ -79,12 +81,17 @@ import { updateDefaultSession } from './updateDefaultSession';
|
|||
import { PreventDisplaySleepService } from './PreventDisplaySleepService';
|
||||
import { SystemTrayService, focusAndForceToTop } from './SystemTrayService';
|
||||
import { SystemTraySettingCache } from './SystemTraySettingCache';
|
||||
import { OptionalResourceService } from './OptionalResourceService';
|
||||
import { EmojiService } from './EmojiService';
|
||||
import {
|
||||
SystemTraySetting,
|
||||
shouldMinimizeToSystemTray,
|
||||
parseSystemTraySetting,
|
||||
} from '../ts/types/SystemTraySetting';
|
||||
import { isSystemTraySupported } from '../ts/types/Settings';
|
||||
import {
|
||||
getDefaultSystemTraySetting,
|
||||
isSystemTraySupported,
|
||||
} from '../ts/types/Settings';
|
||||
import * as ephemeralConfig from './ephemeral_config';
|
||||
import * as logging from '../ts/logging/main_process_logging';
|
||||
import { MainSQL } from '../ts/sql/main';
|
||||
|
@ -109,13 +116,15 @@ import { load as loadLocale } from './locale';
|
|||
|
||||
import type { LoggerType } from '../ts/types/Logging';
|
||||
import { HourCyclePreference } from '../ts/types/I18N';
|
||||
import { ScreenShareStatus } from '../ts/types/Calling';
|
||||
import { DBVersionFromFutureError } from '../ts/sql/migrations';
|
||||
import type { ParsedSignalRoute } from '../ts/util/signalRoutes';
|
||||
import { parseSignalRoute } from '../ts/util/signalRoutes';
|
||||
import * as dns from '../ts/util/dns';
|
||||
import { ZoomFactorService } from '../ts/services/ZoomFactorService';
|
||||
|
||||
const STICKER_CREATOR_PARTITION = 'sticker-creator';
|
||||
import { SafeStorageBackendChangeError } from '../ts/types/SafeStorageBackendChangeError';
|
||||
import { LINUX_PASSWORD_STORE_FLAGS } from '../ts/util/linuxPasswordStoreFlags';
|
||||
import { getOwn } from '../ts/util/getOwn';
|
||||
|
||||
const animationSettings = systemPreferences.getAnimationSettings();
|
||||
|
||||
|
@ -174,6 +183,8 @@ nativeThemeNotifier.initialize();
|
|||
|
||||
let appStartInitialSpellcheckSetting = true;
|
||||
|
||||
let macInitialOpenUrlRoute: ParsedSignalRoute | undefined;
|
||||
|
||||
const cliParser = createParser({
|
||||
allowUnknown: true,
|
||||
options: [
|
||||
|
@ -203,6 +214,7 @@ const defaultWebPrefs = {
|
|||
const DISABLE_GPU =
|
||||
OS.isLinux() && !process.argv.some(arg => arg === '--enable-gpu');
|
||||
|
||||
const DISABLE_IPV6 = process.argv.some(arg => arg === '--disable-ipv6');
|
||||
const FORCE_ENABLE_CRASH_REPORTS = process.argv.some(
|
||||
arg => arg === '--enable-crash-reports'
|
||||
);
|
||||
|
@ -275,10 +287,19 @@ if (!process.mas) {
|
|||
return true;
|
||||
});
|
||||
|
||||
// This event is received in macOS packaged builds.
|
||||
app.on('open-url', (event, incomingHref) => {
|
||||
event.preventDefault();
|
||||
const route = parseSignalRoute(incomingHref);
|
||||
|
||||
if (route != null) {
|
||||
// When the app isn't open and you click a signal link to open the app, then
|
||||
// this event will emit before mainWindow is ready. We save the value for later.
|
||||
if (mainWindow == null || !mainWindow.webContents) {
|
||||
macInitialOpenUrlRoute = route;
|
||||
return;
|
||||
}
|
||||
|
||||
handleSignalRoute(route);
|
||||
}
|
||||
});
|
||||
|
@ -292,22 +313,18 @@ const sql = new MainSQL();
|
|||
const heicConverter = getHeicConverter();
|
||||
|
||||
async function getSpellCheckSetting(): Promise<boolean> {
|
||||
const fastValue = ephemeralConfig.get('spell-check');
|
||||
if (typeof fastValue === 'boolean') {
|
||||
getLogger().info('got fast spellcheck setting', fastValue);
|
||||
return fastValue;
|
||||
const value = ephemeralConfig.get('spell-check');
|
||||
if (typeof value === 'boolean') {
|
||||
getLogger().info('got fast spellcheck setting', value);
|
||||
return value;
|
||||
}
|
||||
|
||||
const json = await sql.sqlCall('getItemById', 'spell-check');
|
||||
|
||||
// Default to `true` if setting doesn't exist yet
|
||||
const slowValue = typeof json?.value === 'boolean' ? json.value : true;
|
||||
ephemeralConfig.set('spell-check', true);
|
||||
|
||||
ephemeralConfig.set('spell-check', slowValue);
|
||||
getLogger().info('initializing spellcheck setting', true);
|
||||
|
||||
getLogger().info('got slow spellcheck setting', slowValue);
|
||||
|
||||
return slowValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
type GetThemeSettingOptionsType = Readonly<{
|
||||
|
@ -317,29 +334,22 @@ type GetThemeSettingOptionsType = Readonly<{
|
|||
async function getThemeSetting({
|
||||
ephemeralOnly = false,
|
||||
}: GetThemeSettingOptionsType = {}): Promise<ThemeSettingType> {
|
||||
let result: unknown;
|
||||
|
||||
const fastValue = ephemeralConfig.get('theme-setting');
|
||||
if (fastValue !== undefined) {
|
||||
getLogger().info('got fast theme-setting value', fastValue);
|
||||
result = fastValue;
|
||||
const value = ephemeralConfig.get('theme-setting');
|
||||
if (value !== undefined) {
|
||||
getLogger().info('got fast theme-setting value', value);
|
||||
} else if (ephemeralOnly) {
|
||||
return 'system';
|
||||
} else {
|
||||
const json = await sql.sqlCall('getItemById', 'theme-setting');
|
||||
|
||||
result = json?.value;
|
||||
}
|
||||
|
||||
// Default to `system` if setting doesn't exist or is invalid
|
||||
const validatedResult =
|
||||
result === 'light' || result === 'dark' || result === 'system'
|
||||
? result
|
||||
value === 'light' || value === 'dark' || value === 'system'
|
||||
? value
|
||||
: 'system';
|
||||
|
||||
if (fastValue !== validatedResult) {
|
||||
if (value !== validatedResult) {
|
||||
ephemeralConfig.set('theme-setting', validatedResult);
|
||||
getLogger().info('got slow theme-setting value', result);
|
||||
getLogger().info('saving theme-setting value', validatedResult);
|
||||
}
|
||||
|
||||
return validatedResult;
|
||||
|
@ -372,35 +382,31 @@ async function getBackgroundColor(
|
|||
}
|
||||
|
||||
async function getLocaleOverrideSetting(): Promise<string | null> {
|
||||
const fastValue = ephemeralConfig.get('localeOverride');
|
||||
const value = ephemeralConfig.get('localeOverride');
|
||||
// eslint-disable-next-line eqeqeq -- Checking for null explicitly
|
||||
if (typeof fastValue === 'string' || fastValue === null) {
|
||||
getLogger().info('got fast localeOverride setting', fastValue);
|
||||
return fastValue;
|
||||
if (typeof value === 'string' || value === null) {
|
||||
getLogger().info('got fast localeOverride setting', value);
|
||||
return value;
|
||||
}
|
||||
|
||||
const json = await sql.sqlCall('getItemById', 'localeOverride');
|
||||
|
||||
// Default to `null` if setting doesn't exist yet
|
||||
const slowValue = typeof json?.value === 'string' ? json.value : null;
|
||||
ephemeralConfig.set('localeOverride', null);
|
||||
|
||||
ephemeralConfig.set('localeOverride', slowValue);
|
||||
getLogger().info('initializing localeOverride setting', null);
|
||||
|
||||
getLogger().info('got slow localeOverride setting', slowValue);
|
||||
|
||||
return slowValue;
|
||||
return null;
|
||||
}
|
||||
|
||||
const zoomFactorService = new ZoomFactorService({
|
||||
async getZoomFactorSetting() {
|
||||
const item = await sql.sqlCall('getItemById', 'zoomFactor');
|
||||
const item = await sql.sqlRead('getItemById', 'zoomFactor');
|
||||
if (typeof item?.value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return item.value;
|
||||
},
|
||||
async setZoomFactorSetting(zoomFactor) {
|
||||
await sql.sqlCall('createOrUpdateItem', {
|
||||
await sql.sqlWrite('createOrUpdateItem', {
|
||||
id: 'zoomFactor',
|
||||
value: zoomFactor,
|
||||
});
|
||||
|
@ -409,10 +415,8 @@ const zoomFactorService = new ZoomFactorService({
|
|||
|
||||
let systemTrayService: SystemTrayService | undefined;
|
||||
const systemTraySettingCache = new SystemTraySettingCache(
|
||||
sql,
|
||||
ephemeralConfig,
|
||||
process.argv,
|
||||
app.getVersion()
|
||||
process.argv
|
||||
);
|
||||
|
||||
const windowFromUserConfig = userConfig.get('window');
|
||||
|
@ -672,10 +676,19 @@ async function createWindow() {
|
|||
const usePreloadBundle =
|
||||
!isTestEnvironment(getEnvironment()) || forcePreloadBundle;
|
||||
|
||||
const primaryDisplay = screen.getPrimaryDisplay();
|
||||
const { width: maxWidth, height: maxHeight } = primaryDisplay.workAreaSize;
|
||||
const width = windowConfig
|
||||
? Math.min(windowConfig.width, maxWidth)
|
||||
: DEFAULT_WIDTH;
|
||||
const height = windowConfig
|
||||
? Math.min(windowConfig.height, maxHeight)
|
||||
: DEFAULT_HEIGHT;
|
||||
|
||||
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
||||
show: false,
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
width,
|
||||
height,
|
||||
minWidth: MIN_WIDTH,
|
||||
minHeight: MIN_HEIGHT,
|
||||
autoHideMenuBar: false,
|
||||
|
@ -700,7 +713,7 @@ async function createWindow() {
|
|||
disableBlinkFeatures: 'Accelerated2dCanvas,AcceleratedSmallCanvases',
|
||||
},
|
||||
icon: windowIcon,
|
||||
...pick(windowConfig, ['autoHideMenuBar', 'width', 'height', 'x', 'y']),
|
||||
...pick(windowConfig, ['autoHideMenuBar', 'x', 'y']),
|
||||
};
|
||||
|
||||
if (!isNumber(windowOptions.width) || windowOptions.width < MIN_WIDTH) {
|
||||
|
@ -880,15 +893,18 @@ async function createWindow() {
|
|||
* if the user is in fullscreen mode and closes the window, not the
|
||||
* application, we need them leave fullscreen first before closing it to
|
||||
* prevent a black screen.
|
||||
* Also check for mainWindow because it might become undefined while
|
||||
* waiting for close confirmation.
|
||||
*
|
||||
* issue: https://github.com/signalapp/Signal-Desktop/issues/4348
|
||||
*/
|
||||
|
||||
if (mainWindow.isFullScreen()) {
|
||||
mainWindow.once('leave-full-screen', () => mainWindow?.hide());
|
||||
mainWindow.setFullScreen(false);
|
||||
} else {
|
||||
mainWindow.hide();
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isFullScreen()) {
|
||||
mainWindow.once('leave-full-screen', () => mainWindow?.hide());
|
||||
mainWindow.setFullScreen(false);
|
||||
} else {
|
||||
mainWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// On Mac, or on other platforms when the tray icon is in use, the window
|
||||
|
@ -896,7 +912,11 @@ async function createWindow() {
|
|||
const usingTrayIcon = shouldMinimizeToSystemTray(
|
||||
await systemTraySettingCache.get()
|
||||
);
|
||||
if (!windowState.shouldQuit() && (usingTrayIcon || OS.isMacOS())) {
|
||||
if (
|
||||
mainWindow &&
|
||||
!windowState.shouldQuit() &&
|
||||
(usingTrayIcon || OS.isMacOS())
|
||||
) {
|
||||
if (usingTrayIcon) {
|
||||
const shownTrayNotice = ephemeralConfig.get('shown-tray-notice');
|
||||
if (shownTrayNotice) {
|
||||
|
@ -1011,21 +1031,24 @@ ipc.handle('database-ready', async () => {
|
|||
getLogger().info('sending `database-ready`');
|
||||
});
|
||||
|
||||
ipc.handle('get-art-creator-auth', () => {
|
||||
const { promise, resolve } = explodePromise<unknown>();
|
||||
strictAssert(mainWindow, 'Main window did not exist');
|
||||
ipc.handle(
|
||||
'art-creator:uploadStickerPack',
|
||||
(_event: Electron.Event, data: unknown) => {
|
||||
const { promise, resolve } = explodePromise<unknown>();
|
||||
strictAssert(mainWindow, 'Main window did not exist');
|
||||
|
||||
mainWindow.webContents.send('open-art-creator');
|
||||
mainWindow.webContents.send('art-creator:uploadStickerPack', data);
|
||||
|
||||
ipc.handleOnce('open-art-creator', (_event, { username, password }) => {
|
||||
resolve({
|
||||
baseUrl: config.get<string>('artCreatorUrl'),
|
||||
username,
|
||||
password,
|
||||
ipc.once('art-creator:uploadStickerPack:done', (_doneEvent, response) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
return promise;
|
||||
}
|
||||
);
|
||||
|
||||
ipc.on('art-creator:onUploadProgress', () => {
|
||||
stickerCreatorWindow?.webContents.send('art-creator:onUploadProgress');
|
||||
});
|
||||
|
||||
ipc.on('show-window', () => {
|
||||
|
@ -1091,12 +1114,17 @@ async function readyForUpdates() {
|
|||
|
||||
isReadyForUpdates = true;
|
||||
|
||||
// First, install requested sticker pack
|
||||
// First, handle requested signal URLs
|
||||
const incomingHref = maybeGetIncomingSignalRoute(process.argv);
|
||||
if (incomingHref) {
|
||||
handleSignalRoute(incomingHref);
|
||||
} else if (macInitialOpenUrlRoute) {
|
||||
handleSignalRoute(macInitialOpenUrlRoute);
|
||||
}
|
||||
|
||||
// Discard value even if we don't handle a saved URL.
|
||||
macInitialOpenUrlRoute = undefined;
|
||||
|
||||
// Second, start checking for app updates
|
||||
try {
|
||||
strictAssert(
|
||||
|
@ -1247,6 +1275,67 @@ async function showScreenShareWindow(sourceName: string) {
|
|||
);
|
||||
}
|
||||
|
||||
let callingDevToolsWindow: BrowserWindow | undefined;
|
||||
async function showCallingDevToolsWindow() {
|
||||
if (callingDevToolsWindow) {
|
||||
callingDevToolsWindow.show();
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
height: 1200,
|
||||
width: 1000,
|
||||
alwaysOnTop: false,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: '#ffffff',
|
||||
darkTheme: false,
|
||||
frame: true,
|
||||
fullscreenable: true,
|
||||
maximizable: true,
|
||||
minimizable: true,
|
||||
resizable: true,
|
||||
show: false,
|
||||
title: getResolvedMessagesLocale().i18n('icu:callingDeveloperTools'),
|
||||
titleBarStyle: nonMainTitleBarStyle,
|
||||
webPreferences: {
|
||||
...defaultWebPrefs,
|
||||
nodeIntegration: false,
|
||||
nodeIntegrationInWorker: false,
|
||||
sandbox: true,
|
||||
contextIsolation: true,
|
||||
nativeWindowOpen: true,
|
||||
preload: join(__dirname, '../bundles/calling-tools/preload.js'),
|
||||
},
|
||||
};
|
||||
|
||||
callingDevToolsWindow = new BrowserWindow(options);
|
||||
|
||||
await handleCommonWindowEvents(callingDevToolsWindow);
|
||||
|
||||
callingDevToolsWindow.once('closed', () => {
|
||||
callingDevToolsWindow = undefined;
|
||||
|
||||
mainWindow?.webContents.send('calling:set-rtc-stats-interval', null);
|
||||
});
|
||||
|
||||
ipc.on('calling:set-rtc-stats-interval', (_, intervalMillis: number) => {
|
||||
mainWindow?.webContents.send(
|
||||
'calling:set-rtc-stats-interval',
|
||||
intervalMillis
|
||||
);
|
||||
});
|
||||
|
||||
ipc.on('calling:rtc-stats-report', (_, report) => {
|
||||
callingDevToolsWindow?.webContents.send('calling:rtc-stats-report', report);
|
||||
});
|
||||
|
||||
await safeLoadURL(
|
||||
callingDevToolsWindow,
|
||||
await prepareFileUrl([__dirname, '../calling_tools.html'])
|
||||
);
|
||||
callingDevToolsWindow.show();
|
||||
}
|
||||
|
||||
let aboutWindow: BrowserWindow | undefined;
|
||||
async function showAbout() {
|
||||
if (aboutWindow) {
|
||||
|
@ -1347,8 +1436,8 @@ async function showSettingsWindow() {
|
|||
|
||||
async function getIsLinked() {
|
||||
try {
|
||||
const number = await sql.sqlCall('getItemById', 'number_id');
|
||||
const password = await sql.sqlCall('getItemById', 'password');
|
||||
const number = await sql.sqlRead('getItemById', 'number_id');
|
||||
const password = await sql.sqlRead('getItemById', 'password');
|
||||
return Boolean(number && password);
|
||||
} catch (e) {
|
||||
return false;
|
||||
|
@ -1514,7 +1603,7 @@ const runSQLCorruptionHandler = async () => {
|
|||
`Restarting the application immediately. Error: ${error.message}`
|
||||
);
|
||||
|
||||
await onDatabaseError(Errors.toLogFormat(error));
|
||||
await onDatabaseError(error);
|
||||
};
|
||||
|
||||
const runSQLReadonlyHandler = async () => {
|
||||
|
@ -1531,31 +1620,139 @@ const runSQLReadonlyHandler = async () => {
|
|||
throw error;
|
||||
};
|
||||
|
||||
async function initializeSQL(
|
||||
userDataPath: string
|
||||
): Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> {
|
||||
let key: string | undefined;
|
||||
const keyFromConfig = userConfig.get('key');
|
||||
if (typeof keyFromConfig === 'string') {
|
||||
key = keyFromConfig;
|
||||
} else if (keyFromConfig) {
|
||||
getLogger().warn(
|
||||
"initializeSQL: got key from config, but it wasn't a string"
|
||||
function generateSQLKey(): string {
|
||||
getLogger().info(
|
||||
'key/initialize: Generating new encryption key, since we did not find it on disk'
|
||||
);
|
||||
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
function getSQLKey(): string {
|
||||
let update = false;
|
||||
const isLinux = OS.isLinux();
|
||||
const legacyKeyValue = userConfig.get('key');
|
||||
const modernKeyValue = userConfig.get('encryptedKey');
|
||||
const previousBackend = isLinux
|
||||
? userConfig.get('safeStorageBackend')
|
||||
: undefined;
|
||||
|
||||
const safeStorageBackend: string | undefined = isLinux
|
||||
? safeStorage.getSelectedStorageBackend()
|
||||
: undefined;
|
||||
const isEncryptionAvailable =
|
||||
safeStorage.isEncryptionAvailable() &&
|
||||
(!isLinux || safeStorageBackend !== 'basic_text');
|
||||
|
||||
// On Linux the backend can change based on desktop environment and command line flags.
|
||||
// If the backend changes we won't be able to decrypt the key.
|
||||
if (
|
||||
isLinux &&
|
||||
typeof previousBackend === 'string' &&
|
||||
previousBackend !== safeStorageBackend
|
||||
) {
|
||||
console.error(
|
||||
`Detected change in safeStorage backend, can't decrypt DB key (previous: ${previousBackend}, current: ${safeStorageBackend})`
|
||||
);
|
||||
throw new SafeStorageBackendChangeError({
|
||||
currentBackend: String(safeStorageBackend),
|
||||
previousBackend,
|
||||
});
|
||||
}
|
||||
if (!key) {
|
||||
getLogger().info(
|
||||
'key/initialize: Generating new encryption key, since we did not find it on disk'
|
||||
);
|
||||
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
|
||||
key = randomBytes(32).toString('hex');
|
||||
|
||||
let key: string;
|
||||
if (typeof modernKeyValue === 'string') {
|
||||
if (!isEncryptionAvailable) {
|
||||
throw new Error("Can't decrypt database key");
|
||||
}
|
||||
|
||||
getLogger().info('getSQLKey: decrypting key');
|
||||
const encrypted = Buffer.from(modernKeyValue, 'hex');
|
||||
key = safeStorage.decryptString(encrypted);
|
||||
|
||||
if (legacyKeyValue != null) {
|
||||
getLogger().info('getSQLKey: removing legacy key');
|
||||
userConfig.set('key', undefined);
|
||||
}
|
||||
|
||||
if (isLinux && previousBackend == null) {
|
||||
getLogger().info(
|
||||
`getSQLKey: saving safeStorageBackend: ${safeStorageBackend}`
|
||||
);
|
||||
userConfig.set('safeStorageBackend', safeStorageBackend);
|
||||
}
|
||||
} else if (typeof legacyKeyValue === 'string') {
|
||||
key = legacyKeyValue;
|
||||
update = isEncryptionAvailable;
|
||||
if (update) {
|
||||
getLogger().info('getSQLKey: migrating key');
|
||||
} else {
|
||||
getLogger().info('getSQLKey: using legacy key');
|
||||
}
|
||||
} else {
|
||||
getLogger().warn("getSQLKey: got key from config, but it wasn't a string");
|
||||
key = generateSQLKey();
|
||||
update = true;
|
||||
}
|
||||
|
||||
if (!update) {
|
||||
return key;
|
||||
}
|
||||
|
||||
if (isEncryptionAvailable) {
|
||||
getLogger().info('getSQLKey: updating encrypted key in the config');
|
||||
const encrypted = safeStorage.encryptString(key).toString('hex');
|
||||
userConfig.set('encryptedKey', encrypted);
|
||||
userConfig.set('key', undefined);
|
||||
|
||||
if (isLinux && safeStorageBackend) {
|
||||
getLogger().info(
|
||||
`getSQLKey: saving safeStorageBackend: ${safeStorageBackend}`
|
||||
);
|
||||
userConfig.set('safeStorageBackend', safeStorageBackend);
|
||||
}
|
||||
} else {
|
||||
getLogger().info('getSQLKey: updating plaintext key in the config');
|
||||
userConfig.set('key', key);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
async function initializeSQL(
|
||||
userDataPath: string
|
||||
): Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> {
|
||||
sqlInitTimeStart = Date.now();
|
||||
|
||||
let key: string;
|
||||
try {
|
||||
key = getSQLKey();
|
||||
} catch (error) {
|
||||
try {
|
||||
// Initialize with *some* key to setup paths
|
||||
await sql.initialize({
|
||||
appVersion: app.getVersion(),
|
||||
configDir: userDataPath,
|
||||
key: 'abcd',
|
||||
logger: getLogger(),
|
||||
});
|
||||
} catch {
|
||||
// Do nothing, we fail right below anyway.
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: new Error(`initializeSQL: Caught a non-error '${error}'`),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// This should be the first awaited call in this function, otherwise
|
||||
// `sql.sqlCall` will throw an uninitialized error instead of waiting for
|
||||
// `sql.sqlRead` will throw an uninitialized error instead of waiting for
|
||||
// init to finish.
|
||||
await sql.initialize({
|
||||
appVersion: app.getVersion(),
|
||||
|
@ -1583,7 +1780,7 @@ async function initializeSQL(
|
|||
return { ok: true, error: undefined };
|
||||
}
|
||||
|
||||
const onDatabaseError = async (error: string) => {
|
||||
const onDatabaseError = async (error: Error) => {
|
||||
// Prevent window from re-opening
|
||||
ready = false;
|
||||
|
||||
|
@ -1601,17 +1798,35 @@ const onDatabaseError = async (error: string) => {
|
|||
const copyErrorAndQuitButtonIndex = 0;
|
||||
const SIGNAL_SUPPORT_LINK = 'https://support.signal.org/error';
|
||||
|
||||
if (error.includes(DBVersionFromFutureError.name)) {
|
||||
if (error instanceof DBVersionFromFutureError) {
|
||||
// If the DB version is too new, the user likely opened an older version of Signal,
|
||||
// and they would almost never want to delete their data as a result, so we don't show
|
||||
// that option
|
||||
messageDetail = i18n('icu:databaseError__startOldVersion');
|
||||
} else if (error instanceof SafeStorageBackendChangeError) {
|
||||
const { currentBackend, previousBackend } = error;
|
||||
const previousBackendFlag = getOwn(
|
||||
LINUX_PASSWORD_STORE_FLAGS,
|
||||
previousBackend
|
||||
);
|
||||
messageDetail = previousBackendFlag
|
||||
? i18n('icu:databaseError__safeStorageBackendChangeWithPreviousFlag', {
|
||||
currentBackend,
|
||||
previousBackend,
|
||||
previousBackendFlag,
|
||||
})
|
||||
: i18n('icu:databaseError__safeStorageBackendChange', {
|
||||
currentBackend,
|
||||
previousBackend,
|
||||
});
|
||||
} else {
|
||||
// Otherwise, this is some other kind of DB error, let's give them the option to
|
||||
// delete.
|
||||
messageDetail = i18n('icu:databaseError__detail', {
|
||||
link: SIGNAL_SUPPORT_LINK,
|
||||
});
|
||||
messageDetail = i18n(
|
||||
'icu:databaseError__detail',
|
||||
{ link: SIGNAL_SUPPORT_LINK },
|
||||
{ bidi: 'strip' }
|
||||
);
|
||||
|
||||
buttons.push(i18n('icu:deleteAndRestart'));
|
||||
deleteAllDataButtonIndex = 1;
|
||||
|
@ -1628,7 +1843,9 @@ const onDatabaseError = async (error: string) => {
|
|||
});
|
||||
|
||||
if (buttonIndex === copyErrorAndQuitButtonIndex) {
|
||||
clipboard.writeText(`Database startup error:\n\n${redactAll(error)}`);
|
||||
clipboard.writeText(
|
||||
`Database startup error:\n\n${redactAll(Errors.toLogFormat(error))}`
|
||||
);
|
||||
} else if (
|
||||
typeof deleteAllDataButtonIndex === 'number' &&
|
||||
buttonIndex === deleteAllDataButtonIndex
|
||||
|
@ -1668,10 +1885,6 @@ let sqlInitPromise:
|
|||
| Promise<{ ok: true; error: undefined } | { ok: false; error: Error }>
|
||||
| undefined;
|
||||
|
||||
ipc.on('database-error', (_event: Electron.Event, error: string) => {
|
||||
drop(onDatabaseError(error));
|
||||
});
|
||||
|
||||
ipc.on('database-readonly', (_event: Electron.Event, error: string) => {
|
||||
// Just let global_errors.ts handle it
|
||||
throw new Error(error);
|
||||
|
@ -1721,21 +1934,32 @@ const featuresToDisable = `HardwareMediaKeyHandling,${app.commandLine.getSwitchV
|
|||
)}`;
|
||||
app.commandLine.appendSwitch('disable-features', featuresToDisable);
|
||||
|
||||
// If we don't set this, Desktop will ask for access to keychain/keyring on startup
|
||||
app.commandLine.appendSwitch('password-store', 'basic');
|
||||
|
||||
// <canvas/> rendering is often utterly broken on Linux when using GPU
|
||||
// acceleration.
|
||||
if (DISABLE_GPU) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
// This has to run before the 'ready' event.
|
||||
electronProtocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: 'attachment',
|
||||
privileges: {
|
||||
supportFetchAPI: true,
|
||||
stream: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
let ready = false;
|
||||
app.on('ready', async () => {
|
||||
dns.setFallback(await getDNSFallback());
|
||||
if (DISABLE_IPV6) {
|
||||
dns.setIPv6Enabled(false);
|
||||
}
|
||||
|
||||
const [userDataPath, crashDumpsPath, installPath] = await Promise.all([
|
||||
realpath(app.getPath('userData')),
|
||||
|
@ -1743,19 +1967,15 @@ app.on('ready', async () => {
|
|||
realpath(app.getAppPath()),
|
||||
]);
|
||||
|
||||
const webSession = session.fromPartition(STICKER_CREATOR_PARTITION);
|
||||
updateDefaultSession(session.defaultSession);
|
||||
|
||||
for (const s of [session.defaultSession, webSession]) {
|
||||
updateDefaultSession(s);
|
||||
|
||||
if (getEnvironment() !== Environment.Test) {
|
||||
installFileHandler({
|
||||
session: s,
|
||||
userDataPath,
|
||||
installPath,
|
||||
isWindows: OS.isWindows(),
|
||||
});
|
||||
}
|
||||
if (getEnvironment() !== Environment.Test) {
|
||||
installFileHandler({
|
||||
session: session.defaultSession,
|
||||
userDataPath,
|
||||
installPath,
|
||||
isWindows: OS.isWindows(),
|
||||
});
|
||||
}
|
||||
|
||||
installWebHandler({
|
||||
|
@ -1763,17 +1983,15 @@ app.on('ready', async () => {
|
|||
session: session.defaultSession,
|
||||
});
|
||||
|
||||
installWebHandler({
|
||||
enableHttp: true,
|
||||
session: webSession,
|
||||
});
|
||||
|
||||
logger = await logging.initialize(getMainWindow);
|
||||
|
||||
// Write buffered information into newly created logger.
|
||||
consoleLogger.writeBufferInto(logger);
|
||||
|
||||
sqlInitPromise = initializeSQL(userDataPath);
|
||||
const resourceService = OptionalResourceService.create(
|
||||
join(userDataPath, 'optionalResources')
|
||||
);
|
||||
await EmojiService.create(resourceService);
|
||||
|
||||
if (!resolvedTranslationsLocale) {
|
||||
preferredSystemLocales = resolveCanonicalLocales(
|
||||
|
@ -1799,24 +2017,21 @@ app.on('ready', async () => {
|
|||
});
|
||||
}
|
||||
|
||||
sqlInitPromise = initializeSQL(userDataPath);
|
||||
|
||||
// First run: configure Signal to minimize to tray. Additionally, on Windows
|
||||
// enable auto-start with start-in-tray so that starting from a Desktop icon
|
||||
// would still show the window.
|
||||
// (User can change these settings later)
|
||||
if (
|
||||
isSystemTraySupported(OS, app.getVersion()) &&
|
||||
isSystemTraySupported(OS) &&
|
||||
(await systemTraySettingCache.get()) === SystemTraySetting.Uninitialized
|
||||
) {
|
||||
const newValue = SystemTraySetting.MinimizeToSystemTray;
|
||||
const newValue = getDefaultSystemTraySetting(OS, app.getVersion());
|
||||
getLogger().info(`app.ready: setting system-tray-setting to ${newValue}`);
|
||||
systemTraySettingCache.set(newValue);
|
||||
|
||||
// Update both stores
|
||||
ephemeralConfig.set('system-tray-setting', newValue);
|
||||
await sql.sqlCall('createOrUpdateItem', {
|
||||
id: 'system-tray-setting',
|
||||
value: newValue,
|
||||
});
|
||||
|
||||
if (OS.isWindows()) {
|
||||
getLogger().info('app.ready: enabling open at login');
|
||||
|
@ -1832,6 +2047,32 @@ app.on('ready', async () => {
|
|||
settingsChannel = new SettingsChannel();
|
||||
settingsChannel.install();
|
||||
|
||||
settingsChannel.on('change:systemTraySetting', async rawSystemTraySetting => {
|
||||
const { openAtLogin } = app.getLoginItemSettings(
|
||||
await getDefaultLoginItemSettings()
|
||||
);
|
||||
|
||||
const systemTraySetting = parseSystemTraySetting(rawSystemTraySetting);
|
||||
systemTraySettingCache.set(systemTraySetting);
|
||||
|
||||
if (systemTrayService) {
|
||||
const isEnabled = shouldMinimizeToSystemTray(systemTraySetting);
|
||||
systemTrayService.setEnabled(isEnabled);
|
||||
}
|
||||
|
||||
// Default login item settings might have changed, so update the object.
|
||||
getLogger().info('refresh-auto-launch: new value', openAtLogin);
|
||||
app.setLoginItemSettings({
|
||||
...(await getDefaultLoginItemSettings()),
|
||||
openAtLogin,
|
||||
});
|
||||
});
|
||||
|
||||
settingsChannel.on(
|
||||
'ephemeral-setting-changed',
|
||||
sendPreferencesChangedEventToWindows
|
||||
);
|
||||
|
||||
// We use this event only a single time to log the startup time of the app
|
||||
// from when it's first ready until the loading screen disappears.
|
||||
ipc.once('signal-app-loaded', (event, info) => {
|
||||
|
@ -1984,7 +2225,7 @@ app.on('ready', async () => {
|
|||
if (sqlError) {
|
||||
getLogger().error('sql.initialize was unsuccessful; returning early');
|
||||
|
||||
await onDatabaseError(Errors.toLogFormat(sqlError));
|
||||
await onDatabaseError(sqlError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -1993,10 +2234,10 @@ app.on('ready', async () => {
|
|||
|
||||
try {
|
||||
const IDB_KEY = 'indexeddb-delete-needed';
|
||||
const item = await sql.sqlCall('getItemById', IDB_KEY);
|
||||
const item = await sql.sqlRead('getItemById', IDB_KEY);
|
||||
if (item && item.value) {
|
||||
await sql.sqlCall('removeIndexedDBFiles');
|
||||
await sql.sqlCall('removeItemById', IDB_KEY);
|
||||
await sql.sqlWrite('removeIndexedDBFiles');
|
||||
await sql.sqlWrite('removeItemById', IDB_KEY);
|
||||
}
|
||||
} catch (err) {
|
||||
getLogger().error(
|
||||
|
@ -2047,6 +2288,7 @@ function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
|
|||
setupAsStandalone,
|
||||
showAbout,
|
||||
showDebugLog: showDebugLogWindow,
|
||||
showCallingDevTools: showCallingDevToolsWindow,
|
||||
showKeyboardShortcuts,
|
||||
showSettings: showSettingsWindow,
|
||||
showWindow,
|
||||
|
@ -2160,12 +2402,15 @@ async function requestShutdown() {
|
|||
// exits the app before we've set everything up in preload() (so the browser isn't
|
||||
// yet listening for these events), or if there are a whole lot of stacked-up tasks.
|
||||
// Note: two minutes is also our timeout for SQL tasks in data.js in the browser.
|
||||
timeout = setTimeout(() => {
|
||||
getLogger().error(
|
||||
'requestShutdown: Response never received; forcing shutdown.'
|
||||
);
|
||||
resolveFn();
|
||||
}, 2 * 60 * 1000);
|
||||
timeout = setTimeout(
|
||||
() => {
|
||||
getLogger().error(
|
||||
'requestShutdown: Response never received; forcing shutdown.'
|
||||
);
|
||||
resolveFn();
|
||||
},
|
||||
2 * 60 * 1000
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
|
@ -2178,11 +2423,19 @@ async function requestShutdown() {
|
|||
function getWindowDebugInfo() {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
|
||||
return {
|
||||
windowCount: windows.length,
|
||||
mainWindowExists: windows.some(win => win === mainWindow),
|
||||
mainWindowIsFullScreen: mainWindow?.isFullScreen(),
|
||||
};
|
||||
try {
|
||||
return {
|
||||
windowCount: windows.length,
|
||||
mainWindowExists: windows.some(win => win === mainWindow),
|
||||
mainWindowIsFullScreen: mainWindow?.isFullScreen(),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
windowCount: 0,
|
||||
mainWindowExists: false,
|
||||
mainWindowIsFullScreen: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
app.on('before-quit', e => {
|
||||
|
@ -2318,36 +2571,21 @@ ipc.on(
|
|||
}
|
||||
);
|
||||
|
||||
ipc.handle(
|
||||
'update-system-tray-setting',
|
||||
async (_event, rawSystemTraySetting /* : Readonly<unknown> */) => {
|
||||
const { openAtLogin } = app.getLoginItemSettings(
|
||||
await getDefaultLoginItemSettings()
|
||||
);
|
||||
|
||||
const systemTraySetting = parseSystemTraySetting(rawSystemTraySetting);
|
||||
systemTraySettingCache.set(systemTraySetting);
|
||||
|
||||
if (systemTrayService) {
|
||||
const isEnabled = shouldMinimizeToSystemTray(systemTraySetting);
|
||||
systemTrayService.setEnabled(isEnabled);
|
||||
ipc.on(
|
||||
'screen-share:status-change',
|
||||
(_event: Electron.Event, status: ScreenShareStatus) => {
|
||||
if (!screenShareWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Default login item settings might have changed, so update the object.
|
||||
getLogger().info('refresh-auto-launch: new value', openAtLogin);
|
||||
app.setLoginItemSettings({
|
||||
...(await getDefaultLoginItemSettings()),
|
||||
openAtLogin,
|
||||
});
|
||||
if (status === ScreenShareStatus.Disconnected) {
|
||||
screenShareWindow.close();
|
||||
} else {
|
||||
screenShareWindow.webContents.send('status-change', status);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ipc.on('close-screen-share-controller', () => {
|
||||
if (screenShareWindow) {
|
||||
screenShareWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('stop-screen-share', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('stop-screen-share');
|
||||
|
@ -2451,7 +2689,6 @@ ipc.on('get-config', async event => {
|
|||
storageUrl: config.get<string>('storageUrl'),
|
||||
updatesUrl: config.get<string>('updatesUrl'),
|
||||
resourcesUrl: config.get<string>('resourcesUrl'),
|
||||
artCreatorUrl: config.get<string>('artCreatorUrl'),
|
||||
cdnUrl0: config.get<string>('cdn.0'),
|
||||
cdnUrl2: config.get<string>('cdn.2'),
|
||||
cdnUrl3: config.get<string>('cdn.3'),
|
||||
|
@ -2460,9 +2697,12 @@ ipc.on('get-config', async event => {
|
|||
!isTestEnvironment(getEnvironment()) && ciMode
|
||||
? Environment.Production
|
||||
: getEnvironment(),
|
||||
isMockTestEnvironment: Boolean(process.env.MOCK_TEST),
|
||||
ciMode,
|
||||
// Should be already computed and cached at this point
|
||||
dnsFallback: await getDNSFallback(),
|
||||
disableIPv6: DISABLE_IPV6,
|
||||
ciBackupPath: config.get<string | null>('ciBackupPath') || undefined,
|
||||
nodeVersion: process.versions.node,
|
||||
hostname: os.hostname(),
|
||||
osRelease: os.release(),
|
||||
|
@ -2476,6 +2716,7 @@ ipc.on('get-config', async event => {
|
|||
serverPublicParams: config.get<string>('serverPublicParams'),
|
||||
serverTrustRoot: config.get<string>('serverTrustRoot'),
|
||||
genericServerPublicParams: config.get<string>('genericServerPublicParams'),
|
||||
backupServerPublicParams: config.get<string>('backupServerPublicParams'),
|
||||
theme,
|
||||
appStartInitialSpellcheckSetting,
|
||||
|
||||
|
@ -2553,24 +2794,20 @@ ipc.handle('DebugLogs.upload', async (_event, content: string) => {
|
|||
});
|
||||
});
|
||||
|
||||
ipc.on('user-config-key', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.returnValue = userConfig.get('key');
|
||||
});
|
||||
|
||||
ipc.on('get-user-data-path', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.returnValue = app.getPath('userData');
|
||||
});
|
||||
|
||||
// Refresh the settings window whenever preferences change
|
||||
ipc.on('preferences-changed', () => {
|
||||
const sendPreferencesChangedEventToWindows = () => {
|
||||
for (const window of activeWindows) {
|
||||
if (window.webContents) {
|
||||
window.webContents.send('preferences-changed');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
ipc.on('preferences-changed', sendPreferencesChangedEventToWindows);
|
||||
|
||||
function maybeGetIncomingSignalRoute(argv: Array<string>) {
|
||||
for (const arg of argv) {
|
||||
|
@ -2597,11 +2834,6 @@ function handleSignalRoute(route: ParsedSignalRoute) {
|
|||
packId: route.args.packId,
|
||||
packKey: Buffer.from(route.args.packKey, 'hex').toString('base64'),
|
||||
});
|
||||
} else if (route.key === 'artAuth') {
|
||||
mainWindow.webContents.send('authorize-art-creator', {
|
||||
token: route.args.token,
|
||||
pubKeyBase64: route.args.pubKey,
|
||||
});
|
||||
} else if (route.key === 'groupInvites') {
|
||||
mainWindow.webContents.send('show-group-via-link', {
|
||||
value: route.args.inviteCode,
|
||||
|
@ -2887,7 +3119,6 @@ async function showStickerCreatorWindow() {
|
|||
show: false,
|
||||
webPreferences: {
|
||||
...defaultWebPrefs,
|
||||
partition: STICKER_CREATOR_PARTITION,
|
||||
nodeIntegration: false,
|
||||
nodeIntegrationInWorker: false,
|
||||
sandbox: true,
|
||||
|
@ -2916,18 +3147,27 @@ async function showStickerCreatorWindow() {
|
|||
}
|
||||
|
||||
if (isTestEnvironment(getEnvironment())) {
|
||||
ipc.on('ci:test-electron:getArgv', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.returnValue = process.argv;
|
||||
});
|
||||
|
||||
ipc.handle('ci:test-electron:debug', async (_event, info) => {
|
||||
process.stdout.write(`ci:test-electron:debug=${JSON.stringify(info)}\n`);
|
||||
});
|
||||
|
||||
ipc.handle('ci:test-electron:done', async (_event, info) => {
|
||||
if (!process.env.TEST_QUIT_ON_COMPLETE) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipc.handle('ci:test-electron:event', async (_event, event) => {
|
||||
process.stdout.write(
|
||||
`ci:test-electron:done=${JSON.stringify(info)}\n`,
|
||||
() => app.quit()
|
||||
`ci:test-electron:event=${JSON.stringify(event)}\n`,
|
||||
() => {
|
||||
if (event.type !== 'end') {
|
||||
return;
|
||||
}
|
||||
if (!process.env.TEST_QUIT_ON_COMPLETE) {
|
||||
return;
|
||||
}
|
||||
app.quit();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ export const createTemplate = (
|
|||
forceUpdate,
|
||||
showAbout,
|
||||
showDebugLog,
|
||||
showCallingDevTools,
|
||||
showKeyboardShortcuts,
|
||||
showSettings,
|
||||
openArtCreator,
|
||||
|
@ -146,6 +147,10 @@ export const createTemplate = (
|
|||
role: 'toggleDevTools' as const,
|
||||
label: i18n('icu:viewMenuToggleDevTools'),
|
||||
},
|
||||
{
|
||||
label: i18n('icu:viewMenuOpenCallingDevTools'),
|
||||
click: showCallingDevTools,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(devTools && platform !== 'linux'
|
||||
|
|
|
@ -31,6 +31,12 @@ function _createPermissionHandler(
|
|||
// We default 'media' permission to false, but the user can override that for
|
||||
// the microphone and camera.
|
||||
if (permission === 'media') {
|
||||
// Pacifying typescript because it is always there for 'media' permission
|
||||
if (!('mediaTypes' in details)) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
details.mediaTypes?.includes('audio') ||
|
||||
details.mediaTypes?.includes('video')
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import { Menu, clipboard, nativeImage } from 'electron';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||
|
||||
import { maybeParseUrl } from '../ts/util/url';
|
||||
|
@ -12,8 +11,9 @@ import type { MenuListType } from '../ts/types/menu';
|
|||
import type { LocalizerType } from '../ts/types/Util';
|
||||
import { strictAssert } from '../ts/util/assert';
|
||||
import type { LoggerType } from '../ts/types/Logging';
|
||||
import { handleAttachmentRequest } from './attachment_channel';
|
||||
|
||||
export const FAKE_DEFAULT_LOCALE = 'en-x-ignore'; // -x- is an extension space for attaching other metadata to the locale
|
||||
export const FAKE_DEFAULT_LOCALE = 'und'; // 'und' is the BCP 47 subtag for "undetermined"
|
||||
|
||||
strictAssert(
|
||||
new Intl.Locale(FAKE_DEFAULT_LOCALE).toString() === FAKE_DEFAULT_LOCALE,
|
||||
|
@ -151,23 +151,35 @@ export const setup = (
|
|||
};
|
||||
label = i18n('icu:contextMenuCopyLink');
|
||||
} else if (isImage) {
|
||||
const urlIsViewOnce =
|
||||
params.srcURL?.includes('/temp/') ||
|
||||
params.srcURL?.includes('\\temp\\');
|
||||
if (urlIsViewOnce) {
|
||||
return;
|
||||
}
|
||||
|
||||
click = () => {
|
||||
click = async () => {
|
||||
const parsedSrcUrl = maybeParseUrl(params.srcURL);
|
||||
if (!parsedSrcUrl || parsedSrcUrl.protocol !== 'file:') {
|
||||
if (!parsedSrcUrl || parsedSrcUrl.protocol !== 'attachment:') {
|
||||
return;
|
||||
}
|
||||
|
||||
const image = nativeImage.createFromPath(
|
||||
fileURLToPath(params.srcURL)
|
||||
);
|
||||
clipboard.writeImage(image);
|
||||
const urlIsViewOnce =
|
||||
parsedSrcUrl.searchParams.get('disposition') === 'temporary';
|
||||
if (urlIsViewOnce) {
|
||||
return;
|
||||
}
|
||||
|
||||
const req = new Request(parsedSrcUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await handleAttachmentRequest(req);
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const image = nativeImage.createFromBuffer(
|
||||
Buffer.from(await res.arrayBuffer())
|
||||
);
|
||||
clipboard.writeImage(image);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load image', error);
|
||||
}
|
||||
};
|
||||
label = i18n('icu:contextMenuCopyImage');
|
||||
} else {
|
||||
|
|
|
@ -7,14 +7,22 @@ import type { MainSQL } from '../ts/sql/main';
|
|||
import { remove as removeUserConfig } from './user_config';
|
||||
import { remove as removeEphemeralConfig } from './ephemeral_config';
|
||||
|
||||
let sql: Pick<MainSQL, 'sqlCall'> | undefined;
|
||||
let sql:
|
||||
| Pick<
|
||||
MainSQL,
|
||||
'sqlRead' | 'sqlWrite' | 'pauseWriteAccess' | 'resumeWriteAccess'
|
||||
>
|
||||
| undefined;
|
||||
|
||||
let initialized = false;
|
||||
|
||||
const SQL_CHANNEL_KEY = 'sql-channel';
|
||||
const SQL_READ_KEY = 'sql-channel:read';
|
||||
const SQL_WRITE_KEY = 'sql-channel:write';
|
||||
const ERASE_SQL_KEY = 'erase-sql-key';
|
||||
const PAUSE_WRITE_ACCESS = 'pause-sql-writes';
|
||||
const RESUME_WRITE_ACCESS = 'resume-sql-writes';
|
||||
|
||||
export function initialize(mainSQL: Pick<MainSQL, 'sqlCall'>): void {
|
||||
export function initialize(mainSQL: typeof sql): void {
|
||||
if (initialized) {
|
||||
throw new Error('sqlChannels: already initialized!');
|
||||
}
|
||||
|
@ -22,15 +30,36 @@ export function initialize(mainSQL: Pick<MainSQL, 'sqlCall'>): void {
|
|||
|
||||
sql = mainSQL;
|
||||
|
||||
ipcMain.handle(SQL_CHANNEL_KEY, (_event, callName, ...args) => {
|
||||
ipcMain.handle(SQL_READ_KEY, (_event, callName, ...args) => {
|
||||
if (!sql) {
|
||||
throw new Error(`${SQL_CHANNEL_KEY}: Not yet initialized!`);
|
||||
throw new Error(`${SQL_READ_KEY}: Not yet initialized!`);
|
||||
}
|
||||
return sql.sqlCall(callName, ...args);
|
||||
return sql.sqlRead(callName, ...args);
|
||||
});
|
||||
|
||||
ipcMain.handle(SQL_WRITE_KEY, (_event, callName, ...args) => {
|
||||
if (!sql) {
|
||||
throw new Error(`${SQL_WRITE_KEY}: Not yet initialized!`);
|
||||
}
|
||||
return sql.sqlWrite(callName, ...args);
|
||||
});
|
||||
|
||||
ipcMain.handle(ERASE_SQL_KEY, () => {
|
||||
removeUserConfig();
|
||||
removeEphemeralConfig();
|
||||
});
|
||||
|
||||
ipcMain.handle(PAUSE_WRITE_ACCESS, () => {
|
||||
if (!sql) {
|
||||
throw new Error(`${PAUSE_WRITE_ACCESS}: Not yet initialized!`);
|
||||
}
|
||||
return sql.pauseWriteAccess();
|
||||
});
|
||||
|
||||
ipcMain.handle(RESUME_WRITE_ACCESS, () => {
|
||||
if (!sql) {
|
||||
throw new Error(`${PAUSE_WRITE_ACCESS}: Not yet initialized!`);
|
||||
}
|
||||
return sql.resumeWriteAccess();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!-- Copyright 2014 Signal Messenger, LLC -->
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
@ -16,12 +16,12 @@
|
|||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none';
|
||||
child-src 'self';
|
||||
connect-src 'self' https: wss:;
|
||||
connect-src 'self' https: wss: attachment:;
|
||||
font-src 'self';
|
||||
form-action 'self';
|
||||
frame-src 'none';
|
||||
img-src 'self' blob: data:;
|
||||
media-src 'self' blob:;
|
||||
img-src 'self' blob: data: emoji: attachment:;
|
||||
media-src 'self' blob: attachment:;
|
||||
object-src 'none';
|
||||
script-src 'self' 'sha256-Qu05oqDmBO5fZacm7tr/oerJcqsW0G/XqP4PRCziovc=' 'sha256-eLeGwSfPmXJ+EUiLfIeXABvLiUqDbiKgNLpHITaabgQ=';
|
||||
style-src 'self' 'unsafe-inline';"
|
||||
|
@ -96,7 +96,7 @@
|
|||
<div class="module-title-bar-drag-area"></div>
|
||||
|
||||
<div class="module-splash-screen__logo module-img--150"></div>
|
||||
<div class="container">
|
||||
<div class="dot-container">
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
|
|
|
@ -13,8 +13,8 @@ LangString signalMinAppVersionErr 1031 "Eine neuere Version von Signal ist berei
|
|||
LangString signalMinWinVersionErr 1036 "La version Desktop de Signal ne fonctionne plus sur cet ordinateur. Pour continuer d’utiliser la version Desktop de Signal, veuillez mettre à jour la version Windows de votre ordinateur."
|
||||
LangString signalMinAppVersionErr 1036 "Une nouvelle version de Signal est déjà installée. Êtes-vous sûr de vouloir continuer ?"
|
||||
# es_ES
|
||||
LangString signalMinWinVersionErr 3082 "Signal para Escritorio ya no funciona en este ordenador. Para volver a usar Signal para Escritorio, actualiza la versión de Windows de tu ordenador."
|
||||
LangString signalMinAppVersionErr 3082 "Ya está instalada una versión más reciente de Signal. ¿Segurx que quieres continuar?"
|
||||
LangString signalMinWinVersionErr 3082 "Signal Desktop ya no funciona en este ordenador. Para volver a usar Signal en tu scritorio, actualiza la versión de Windows de tu ordenador."
|
||||
LangString signalMinAppVersionErr 3082 "Ya está instalada una versión más reciente de Signal. ¿Continuar de todos modos?"
|
||||
# zh_CN
|
||||
LangString signalMinWinVersionErr 2052 "Signal desktop 无法在此电脑上运行。如您希望再次使用 Signal desktop,请更新您电脑的 Windows 版本。"
|
||||
LangString signalMinAppVersionErr 2052 "更新版 Signal 已安装完毕。您确定要继续吗?"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue