Merge remote-tracking branch 'github.com/develop' into develop
# Conflicts: # .github/workflows/build_and_deploy.yaml # .github/workflows/build_and_test.yaml # .github/workflows/build_linux.yaml # .github/workflows/build_macos.yaml # .github/workflows/build_prepare.yaml # .github/workflows/dockerbuild.yaml # .github/workflows/static_analysis.yaml # .github/workflows/triage-stale.yml # package.json
This commit is contained in:
298
.github/workflows/build_and_deploy.yaml
vendored
Normal file
298
.github/workflows/build_and_deploy.yaml
vendored
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
on:
|
||||||
|
# Nightly build
|
||||||
|
schedule:
|
||||||
|
- cron: "0 9 * * *"
|
||||||
|
# Release build
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
# Manual nightly & release
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
mode:
|
||||||
|
description: What type of build to trigger. Release builds MUST be ran from the `master` branch.
|
||||||
|
required: true
|
||||||
|
default: nightly
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- nightly
|
||||||
|
- release
|
||||||
|
macos:
|
||||||
|
description: Build macOS
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
windows:
|
||||||
|
description: Build Windows
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
linux:
|
||||||
|
description: Build Linux
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
deploy:
|
||||||
|
description: Deploy artifacts
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
run-name: Element ${{ inputs.mode != 'release' && github.event_name != 'release' && 'Nightly' || 'Desktop' }}
|
||||||
|
concurrency: ${{ github.workflow }}
|
||||||
|
env:
|
||||||
|
R2_BUCKET: ${{ vars.R2_BUCKET }}
|
||||||
|
permissions: {} # Uses ELEMENT_BOT_TOKEN
|
||||||
|
jobs:
|
||||||
|
prepare:
|
||||||
|
uses: ./.github/workflows/build_prepare.yaml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
with:
|
||||||
|
config: element.io/${{ inputs.mode || (github.event_name == 'release' && 'release') || 'nightly' }}
|
||||||
|
version: ${{ (inputs.mode != 'release' && github.event_name != 'release') && 'develop' || '' }}
|
||||||
|
nightly: ${{ inputs.mode != 'release' && github.event_name != 'release' }}
|
||||||
|
deploy: ${{ inputs.deploy || (github.event_name != 'workflow_dispatch' && github.event.release.prerelease != true) }}
|
||||||
|
secrets:
|
||||||
|
CF_R2_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||||
|
CF_R2_TOKEN: ${{ secrets.CF_R2_TOKEN }}
|
||||||
|
|
||||||
|
windows:
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.windows
|
||||||
|
needs: prepare
|
||||||
|
name: Windows ${{ matrix.arch }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [x64, arm64]
|
||||||
|
uses: ./.github/workflows/build_windows.yaml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
sign: true
|
||||||
|
arch: ${{ matrix.arch }}
|
||||||
|
version: ${{ needs.prepare.outputs.nightly-version }}
|
||||||
|
|
||||||
|
macos:
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.macos
|
||||||
|
needs: prepare
|
||||||
|
name: macOS
|
||||||
|
uses: ./.github/workflows/build_macos.yaml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
sign: true
|
||||||
|
base-url: https://packages.element.io/${{ needs.prepare.outputs.packages-dir }}
|
||||||
|
version: ${{ needs.prepare.outputs.nightly-version }}
|
||||||
|
|
||||||
|
linux:
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.linux
|
||||||
|
needs: prepare
|
||||||
|
name: Linux ${{ matrix.arch }} (sqlcipher ${{ matrix.sqlcipher }})
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [amd64, arm64]
|
||||||
|
sqlcipher: [static]
|
||||||
|
uses: ./.github/workflows/build_linux.yaml
|
||||||
|
with:
|
||||||
|
arch: ${{ matrix.arch }}
|
||||||
|
sqlcipher: ${{ matrix.sqlcipher }}
|
||||||
|
version: ${{ needs.prepare.outputs.nightly-version }}
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs:
|
||||||
|
- prepare
|
||||||
|
- macos
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
name: ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }}
|
||||||
|
if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
|
||||||
|
environment: ${{ needs.prepare.outputs.deploy == 'true' && 'packages.element.io' || '' }}
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
|
||||||
|
- name: Prepare artifacts for deployment
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
for arch in x64 arm64
|
||||||
|
do
|
||||||
|
if [ -d "win-$arch" ]; then
|
||||||
|
mkdir -p packages.element.io/{install,update}/win32/$arch
|
||||||
|
mv win-$arch/squirrel-windows*/*.exe "packages.element.io/install/win32/$arch/"
|
||||||
|
mv win-$arch/squirrel-windows*/*.nupkg "packages.element.io/update/win32/$arch/"
|
||||||
|
mv win-$arch/squirrel-windows*/RELEASES "packages.element.io/update/win32/$arch/"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
if [ -d macos ]; then
|
||||||
|
mkdir -p packages.element.io/{install,update}/macos
|
||||||
|
mv macos/*.dmg packages.element.io/install/macos/
|
||||||
|
mv macos/*-mac.zip packages.element.io/update/macos/
|
||||||
|
mv macos/*.json packages.element.io/update/macos/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
if [ -d linux-amd64-sqlcipher-static ]; then
|
||||||
|
mkdir -p packages.element.io/install/linux/glibc-x86-64
|
||||||
|
mv linux-amd64-sqlcipher-static/*.tar.gz packages.element.io/install/linux/glibc-x86-64
|
||||||
|
fi
|
||||||
|
if [ -d linux-arm64-sqlcipher-static ]; then
|
||||||
|
mkdir -p packages.element.io/install/linux/glibc-aarch64
|
||||||
|
mv linux-arm64-sqlcipher-static/*.tar.gz packages.element.io/install/linux/glibc-aarch64
|
||||||
|
fi
|
||||||
|
|
||||||
|
# We don't wish to store the installer for every nightly ever, so we only keep the latest
|
||||||
|
- name: "[Nightly] Strip version from installer file"
|
||||||
|
if: needs.prepare.outputs.nightly-version != ''
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
for arch in x64 arm64
|
||||||
|
do
|
||||||
|
[ -d "win-$arch" ] && mv packages.element.io/install/win32/$arch/{*,"Element Nightly Setup"}.exe
|
||||||
|
done
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
[ -d macos ] && mv packages.element.io/install/macos/{*,"Element Nightly"}.dmg
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
[ -d linux-amd64-sqlcipher-static ] && mv packages.element.io/install/linux/glibc-x86-64/{*,element-desktop-nightly}.tar.gz
|
||||||
|
[ -d linux-arm64-sqlcipher-static ] && mv packages.element.io/install/linux/glibc-aarch64/{*,element-desktop-nightly}.tar.gz
|
||||||
|
|
||||||
|
- name: "[Release] Prepare release latest symlink"
|
||||||
|
if: needs.prepare.outputs.nightly-version == ''
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
for arch in x64 arm64
|
||||||
|
do
|
||||||
|
if [ -d "win-$arch" ]; then
|
||||||
|
pushd packages.element.io/install/win32/$arch
|
||||||
|
ln -s "$(find . -type f -iname "*.exe" | xargs -0 -n1 -- basename)" "Element Setup.exe"
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
if [ -d macos ]; then
|
||||||
|
pushd packages.element.io/install/macos
|
||||||
|
ln -s "$(find . -type f -iname "*.dmg" | xargs -0 -n1 -- basename)" "Element.dmg"
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
if [ -d linux-amd64-sqlcipher-static ]; then
|
||||||
|
pushd packages.element.io/install/linux/glibc-x86-64
|
||||||
|
ln -s "$(find . -type f -iname "*.tar.gz" | xargs -0 -n1 -- basename)" "element-desktop.tar.gz"
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
if [ -d linux-arm64-sqlcipher-static ]; then
|
||||||
|
pushd packages.element.io/install/linux/glibc-aarch64
|
||||||
|
ln -s "$(find . -type f -iname "*.tar.gz" | xargs -0 -n1 -- basename)" "element-desktop.tar.gz"
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Stash packages.element.io
|
||||||
|
if: needs.prepare.outputs.deploy == 'false'
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: packages.element.io
|
||||||
|
path: packages.element.io
|
||||||
|
|
||||||
|
# Checksum algorithm specified as per https://developers.cloudflare.com/r2/examples/aws/aws-cli/
|
||||||
|
- name: Deploy artifacts
|
||||||
|
if: needs.prepare.outputs.deploy == 'true'
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
aws s3 cp --recursive packages.element.io/ s3://$R2_BUCKET/$DEPLOYMENT_DIR --endpoint-url $R2_URL --region auto --checksum-algorithm CRC32
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
||||||
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
|
DEPLOYMENT_DIR: ${{ needs.prepare.outputs.packages-dir }}
|
||||||
|
|
||||||
|
- name: Notify packages.element.io of new files
|
||||||
|
if: needs.prepare.outputs.deploy == 'true'
|
||||||
|
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
repository: element-hq/packages.element.io
|
||||||
|
event-type: packages-index
|
||||||
|
|
||||||
|
- name: Find debs
|
||||||
|
id: deb
|
||||||
|
if: needs.linux.result == 'success'
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
|
||||||
|
for arch in amd64 arm64
|
||||||
|
do
|
||||||
|
echo "$arch=$(ls linux-$arch-sqlcipher-static/*.deb | tail -n1)" >> $GITHUB_OUTPUT
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Stash debs
|
||||||
|
if: needs.prepare.outputs.deploy == 'false' && needs.linux.result == 'success'
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: debs
|
||||||
|
path: |
|
||||||
|
${{ steps.deb.outputs.amd64 }}
|
||||||
|
${{ steps.deb.outputs.arm64 }}
|
||||||
|
|
||||||
|
- name: Publish amd64 deb to packages.element.io
|
||||||
|
uses: element-hq/packages.element.io@master
|
||||||
|
if: needs.prepare.outputs.deploy == 'true' && needs.linux.result == 'success'
|
||||||
|
with:
|
||||||
|
file: ${{ steps.deb.outputs.amd64 }}
|
||||||
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
bucket-api: ${{ vars.CF_R2_S3_API }}
|
||||||
|
bucket-key-id: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||||
|
bucket-access-key: ${{ secrets.CF_R2_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish arm64 deb to packages.element.io
|
||||||
|
uses: element-hq/packages.element.io@master
|
||||||
|
if: needs.prepare.outputs.deploy == 'true' && needs.linux.result == 'success'
|
||||||
|
with:
|
||||||
|
file: ${{ steps.deb.outputs.arm64 }}
|
||||||
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
bucket-api: ${{ vars.CF_R2_S3_API }}
|
||||||
|
bucket-key-id: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||||
|
bucket-access-key: ${{ secrets.CF_R2_TOKEN }}
|
||||||
|
|
||||||
|
deploy-ess:
|
||||||
|
needs: deploy
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
name: Deploy builds to ESS
|
||||||
|
if: needs.prepare.outputs.deploy == 'true' && github.event_name == 'release'
|
||||||
|
env:
|
||||||
|
BUCKET_NAME: "element-desktop-msi.onprem.element.io"
|
||||||
|
AWS_REGION: "eu-central-1"
|
||||||
|
permissions:
|
||||||
|
id-token: write # This is required for requesting the JWT
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::264135176173:role/Push-ElementDesktop-MSI
|
||||||
|
role-session-name: githubaction-run-${{ github.run_id }}
|
||||||
|
aws-region: ${{ env.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
pattern: win-*
|
||||||
|
|
||||||
|
- name: Copy files to S3
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
|
||||||
|
PREFIX="${VERSION%.*}"
|
||||||
|
for file in win-*/*.msi; do
|
||||||
|
filename=$(basename "$file")
|
||||||
|
aws s3 cp "$file" "s3://${{ env.BUCKET_NAME }}/$PREFIX/$filename"
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.event.release.tag_name }}
|
||||||
119
.github/workflows/build_and_test.yaml
vendored
119
.github/workflows/build_and_test.yaml
vendored
@@ -1,42 +1,85 @@
|
|||||||
name: Build Windows Package
|
name: Build and Test
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request: {}
|
||||||
branches: [develop, master, pipeline-rework]
|
push:
|
||||||
pull_request: {}
|
branches: [develop, staging, master]
|
||||||
workflow_dispatch: {}
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
permissions: {} # No permissions required
|
||||||
jobs:
|
jobs:
|
||||||
fetch:
|
fetch:
|
||||||
uses: ./.github/workflows/build_prepare.yaml
|
uses: ./.github/workflows/build_prepare.yaml
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
with:
|
|
||||||
config: ""
|
|
||||||
version: custom
|
|
||||||
|
|
||||||
setup:
|
|
||||||
runs-on: windows-gp
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version-file: .node-version
|
config: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'element.io/nightly' || 'element.io/release' }}
|
||||||
|
version: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'develop' || '' }}
|
||||||
- name: Install Yarn
|
branch-matching: true
|
||||||
run: npm install -g yarn
|
|
||||||
|
|
||||||
- name: Cache Yarn dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.yarn/cache
|
|
||||||
key: yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
yarn-
|
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
needs: [fetch, setup]
|
needs: fetch
|
||||||
name: Windows Build
|
name: Windows
|
||||||
uses: ./.github/workflows/build_windows.yaml
|
uses: ./.github/workflows/build_windows.yaml
|
||||||
with:
|
strategy:
|
||||||
arch: x64
|
matrix:
|
||||||
|
arch: [x64, ia32, arm64]
|
||||||
|
with:
|
||||||
|
arch: ${{ matrix.arch }}
|
||||||
|
blob_report: true
|
||||||
|
|
||||||
|
linux:
|
||||||
|
needs: fetch
|
||||||
|
name: "Linux (${{ matrix.arch }}) (sqlcipher: ${{ matrix.sqlcipher }})"
|
||||||
|
uses: ./.github/workflows/build_linux.yaml
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
sqlcipher: [system, static]
|
||||||
|
arch: [amd64, arm64]
|
||||||
|
with:
|
||||||
|
sqlcipher: ${{ matrix.sqlcipher }}
|
||||||
|
arch: ${{ matrix.arch }}
|
||||||
|
blob_report: true
|
||||||
|
|
||||||
|
macos:
|
||||||
|
needs: fetch
|
||||||
|
name: macOS
|
||||||
|
uses: ./.github/workflows/build_macos.yaml
|
||||||
|
with:
|
||||||
|
blob_report: true
|
||||||
|
|
||||||
|
tests-done:
|
||||||
|
needs: [windows, linux, macos]
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
cache: "yarn"
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Download blob reports from GitHub Actions Artifacts
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
pattern: blob-report-*
|
||||||
|
path: all-blob-reports
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Merge into HTML Report
|
||||||
|
run: yarn playwright merge-reports -c ./playwright.config.ts --reporter=html ./all-blob-reports
|
||||||
|
|
||||||
|
- name: Upload HTML report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: html-report
|
||||||
|
path: playwright-report
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||||
|
run: exit 1
|
||||||
|
|||||||
201
.github/workflows/build_linux.yaml
vendored
Normal file
201
.github/workflows/build_linux.yaml
vendored
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# This workflow relies on actions/cache to store the hak dependency artifacts as they take a long time to build
|
||||||
|
# Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure
|
||||||
|
# the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch.
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
arch:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: "The architecture to build for, one of 'amd64' | 'arm64'"
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: "Version string to override the one in package.json, used for non-release builds"
|
||||||
|
sqlcipher:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: "How to link sqlcipher, one of 'system' | 'static'"
|
||||||
|
blob_report:
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
description: "Whether to run the blob report"
|
||||||
|
env:
|
||||||
|
SQLCIPHER_BUNDLED: ${{ inputs.sqlcipher == 'static' && '1' || '' }}
|
||||||
|
MAX_GLIBC: 2.31 # bullseye-era glibc, used by glibc-check.sh
|
||||||
|
permissions: {} # No permissions required
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
# We build on native infrastructure as matrix-seshat fails to cross-compile properly
|
||||||
|
# https://github.com/matrix-org/seshat/issues/135
|
||||||
|
runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
|
||||||
|
env:
|
||||||
|
HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-desktop-dockerbuild
|
||||||
|
steps:
|
||||||
|
- name: Resolve docker image tag for push
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:$GITHUB_REF_NAME" >> $GITHUB_ENV
|
||||||
|
- name: Resolve docker image tag for release
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:staging" >> $GITHUB_ENV
|
||||||
|
- name: Resolve docker image tag for other triggers
|
||||||
|
if: github.event_name != 'push' && github.event_name != 'release'
|
||||||
|
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:develop" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: nbucic/variable-mapper@0673f6891a0619ba7c002ecfed0f9f4f39017b6f
|
||||||
|
id: config
|
||||||
|
with:
|
||||||
|
key: "${{ inputs.arch }}"
|
||||||
|
export_to: output
|
||||||
|
map: |
|
||||||
|
{
|
||||||
|
"amd64": {
|
||||||
|
"target": "x86_64-unknown-linux-gnu",
|
||||||
|
"arch": "x86-64"
|
||||||
|
},
|
||||||
|
"arm64": {
|
||||||
|
"target": "aarch64-unknown-linux-gnu",
|
||||||
|
"arch": "aarch64",
|
||||||
|
"build-args": "--arm64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
name: webapp
|
||||||
|
|
||||||
|
- name: Cache .hak
|
||||||
|
id: cache
|
||||||
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||||
|
with:
|
||||||
|
key: ${{ runner.os }}-${{ github.ref_name }}-${{ inputs.sqlcipher }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion', 'dockerbuild/*') }}
|
||||||
|
path: |
|
||||||
|
./.hak
|
||||||
|
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version-file: .node-version
|
||||||
|
cache: "yarn"
|
||||||
|
env:
|
||||||
|
# Workaround for https://github.com/actions/setup-node/issues/317
|
||||||
|
FORCE_COLOR: 0
|
||||||
|
|
||||||
|
- name: Install Deps
|
||||||
|
run: "yarn install --frozen-lockfile"
|
||||||
|
|
||||||
|
- name: "Get modified files"
|
||||||
|
id: changed_files
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request'
|
||||||
|
uses: tj-actions/changed-files@823fcebdb31bb35fdf2229d9f769b400309430d0 # v46
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
dockerbuild/**
|
||||||
|
|
||||||
|
# This allows contributors to test changes to the dockerbuild image within a pull request
|
||||||
|
- name: Build docker image
|
||||||
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||||
|
if: steps.changed_files.outputs.any_modified == 'true'
|
||||||
|
with:
|
||||||
|
file: dockerbuild/Dockerfile
|
||||||
|
load: true
|
||||||
|
platforms: linux/${{ inputs.arch }}
|
||||||
|
tags: ${{ env.HAK_DOCKER_IMAGE }}
|
||||||
|
|
||||||
|
- name: Build Natives
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
docker run \
|
||||||
|
-v ${{ github.workspace }}:/work -w /work \
|
||||||
|
-e SQLCIPHER_BUNDLED \
|
||||||
|
$HAK_DOCKER_IMAGE \
|
||||||
|
yarn build:native
|
||||||
|
|
||||||
|
- name: Fix permissions on .hak
|
||||||
|
run: sudo chown -R $USER:$USER .hak
|
||||||
|
|
||||||
|
- name: Check native libraries in hak dependencies
|
||||||
|
run: |
|
||||||
|
shopt -s globstar
|
||||||
|
|
||||||
|
for filename in ./.hak/hakModules/**/*.node; do
|
||||||
|
./scripts/glibc-check.sh $filename
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Generate debian files and arguments
|
||||||
|
run: |
|
||||||
|
if [ -f changelog.Debian ]; then
|
||||||
|
echo "ED_DEBIAN_CHANGELOG=changelog.Debian" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Workaround for https://github.com/electron-userland/electron-builder/issues/6116
|
||||||
|
- name: Install fpm
|
||||||
|
if: inputs.arch == 'arm64'
|
||||||
|
run: |
|
||||||
|
sudo apt-get install ruby-dev build-essential
|
||||||
|
sudo gem install fpm
|
||||||
|
echo "USE_SYSTEM_FPM=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build App
|
||||||
|
run: yarn build --publish never -l ${{ steps.config.outputs.build-args }}
|
||||||
|
env:
|
||||||
|
# Only set for Nightly builds
|
||||||
|
ED_NIGHTLY: ${{ inputs.version }}
|
||||||
|
|
||||||
|
- name: Check native libraries
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
shopt -s globstar
|
||||||
|
|
||||||
|
FILES=$(file dist/**/*.node)
|
||||||
|
echo "$FILES"
|
||||||
|
|
||||||
|
if [ grep -v "$ARCH" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
LIBS=$(readelf -d dist/**/*.node | grep NEEDED)
|
||||||
|
echo "$LIBS"
|
||||||
|
|
||||||
|
set +x
|
||||||
|
assert_contains_string() { [[ "$1" == *"$2"* ]]; }
|
||||||
|
! assert_contains_string "$LIBS" "libcrypto.so.1.1"
|
||||||
|
if [ "$SQLCIPHER_BUNDLED" == "1" ]; then
|
||||||
|
! assert_contains_string "$LIBS" "libsqlcipher.so.0"
|
||||||
|
else
|
||||||
|
assert_contains_string "$LIBS" "libsqlcipher.so.0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
./scripts/glibc-check.sh dist/linux-*unpacked/element-desktop*
|
||||||
|
env:
|
||||||
|
ARCH: ${{ steps.config.outputs.arch }}
|
||||||
|
|
||||||
|
# We exclude *-unpacked as it loses permissions and the tarball contains it with correct permissions
|
||||||
|
- name: Upload Artifacts
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
|
||||||
|
path: |
|
||||||
|
dist
|
||||||
|
!dist/*-unpacked/**
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
- name: Assert all required files are present
|
||||||
|
run: |
|
||||||
|
test -f ./dist/element-desktop*$ARCH.deb
|
||||||
|
test -f ./dist/element-desktop*.tar.gz
|
||||||
|
env:
|
||||||
|
ARCH: ${{ inputs.arch }}
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: build
|
||||||
|
uses: ./.github/workflows/build_test.yaml
|
||||||
|
with:
|
||||||
|
artifact: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
|
||||||
|
runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
|
||||||
|
executable: /opt/Element*/element-desktop*
|
||||||
|
prepare_cmd: |
|
||||||
|
sudo apt-get -qq update
|
||||||
|
sudo apt install ./dist/*.deb
|
||||||
|
blob_report: ${{ inputs.blob_report }}
|
||||||
166
.github/workflows/build_macos.yaml
vendored
Normal file
166
.github/workflows/build_macos.yaml
vendored
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# This workflow relies on actions/cache to store the hak dependency artifacts as they take a long time to build
|
||||||
|
# Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure
|
||||||
|
# the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch.
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
secrets:
|
||||||
|
APPLE_ID:
|
||||||
|
required: false
|
||||||
|
APPLE_ID_PASSWORD:
|
||||||
|
required: false
|
||||||
|
APPLE_TEAM_ID:
|
||||||
|
required: false
|
||||||
|
APPLE_CSC_KEY_PASSWORD:
|
||||||
|
required: false
|
||||||
|
APPLE_CSC_LINK:
|
||||||
|
required: false
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: "Version string to override the one in package.json, used for non-release builds"
|
||||||
|
sign:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: "Whether to sign & notarise the build, requires 'packages.element.io' environment"
|
||||||
|
base-url:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: "The URL to which the output will be deployed."
|
||||||
|
blob_report:
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
description: "Whether to run the blob report"
|
||||||
|
permissions: {} # No permissions required
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-14 # M1
|
||||||
|
environment: ${{ inputs.sign && 'packages.element.io' || '' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
name: webapp
|
||||||
|
|
||||||
|
- name: Cache .hak
|
||||||
|
id: cache
|
||||||
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||||
|
with:
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('hakHash', 'electronVersion') }}
|
||||||
|
path: |
|
||||||
|
./.hak
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
rustup toolchain install stable --profile minimal --no-self-update
|
||||||
|
rustup default stable
|
||||||
|
rustup target add aarch64-apple-darwin
|
||||||
|
rustup target add x86_64-apple-darwin
|
||||||
|
|
||||||
|
# M1 macos-14 comes without Python preinstalled
|
||||||
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version-file: .node-version
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Install Deps
|
||||||
|
run: "yarn install --frozen-lockfile"
|
||||||
|
|
||||||
|
# Python 3.12 drops distutils which keytar relies on
|
||||||
|
- name: Install setuptools
|
||||||
|
run: pip3 install setuptools
|
||||||
|
|
||||||
|
- name: Build Natives
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: yarn build:native:universal
|
||||||
|
|
||||||
|
# We split these because electron-builder gets upset if we set CSC_LINK even to an empty string
|
||||||
|
- name: "[Signed] Build App"
|
||||||
|
if: inputs.sign != ''
|
||||||
|
run: |
|
||||||
|
yarn build:universal --publish never
|
||||||
|
env:
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CSC_KEY_PASSWORD }}
|
||||||
|
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
|
||||||
|
# Only set for Nightly builds
|
||||||
|
ED_NIGHTLY: ${{ inputs.version }}
|
||||||
|
|
||||||
|
- name: Check app was signed & notarised successfully
|
||||||
|
if: inputs.sign != ''
|
||||||
|
run: |
|
||||||
|
hdiutil attach dist/*.dmg -mountpoint /Volumes/Element
|
||||||
|
codesign -dv --verbose=4 /Volumes/Element/*.app
|
||||||
|
spctl -a -vvv -t install /Volumes/Element/*.app
|
||||||
|
hdiutil detach /Volumes/Element
|
||||||
|
|
||||||
|
- name: "[Unsigned] Build App"
|
||||||
|
if: inputs.sign == ''
|
||||||
|
run: |
|
||||||
|
yarn build:universal --publish never
|
||||||
|
env:
|
||||||
|
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||||
|
|
||||||
|
- name: Generate releases.json
|
||||||
|
if: inputs.base-url
|
||||||
|
run: |
|
||||||
|
PKG_JSON_VERSION=$(cat package.json | jq -r .version)
|
||||||
|
LATEST=$(find dist -type f -iname "*-mac.zip" | xargs -0 -n1 -- basename)
|
||||||
|
# Encode spaces in the URL as Squirrel.Mac complains about bad JSON otherwise
|
||||||
|
URL="${{ inputs.base-url }}/update/macos/${LATEST// /%20}"
|
||||||
|
|
||||||
|
jq -n --arg version "${VERSION:-$PKG_JSON_VERSION}" --arg url "$URL" '
|
||||||
|
{
|
||||||
|
currentRelease: $version,
|
||||||
|
releases: [{
|
||||||
|
version: $version,
|
||||||
|
updateTo: {
|
||||||
|
version: $version,
|
||||||
|
url: $url,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
' > dist/releases.json
|
||||||
|
jq -n --arg url "$URL" '
|
||||||
|
{ url: $url }
|
||||||
|
' > dist/releases-legacy.json
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
|
||||||
|
# We exclude mac-universal as the unpacked app takes forever to upload and zip and dmg already contains it
|
||||||
|
- name: Upload Artifacts
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: macos
|
||||||
|
path: |
|
||||||
|
dist
|
||||||
|
!dist/mac-universal/**
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
- name: Assert all required files are present
|
||||||
|
run: |
|
||||||
|
test -f ./dist/Element*.dmg
|
||||||
|
test -f ./dist/Element*-mac.zip
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: build
|
||||||
|
uses: ./.github/workflows/build_test.yaml
|
||||||
|
with:
|
||||||
|
artifact: macos
|
||||||
|
runs-on: macos-14
|
||||||
|
executable: /Users/runner/Applications/Element*.app/Contents/MacOS/Element*
|
||||||
|
# We need to mount the DMG and copy the app to the Applications folder as a mounted DMG is
|
||||||
|
# read-only and thus would not allow us to override the fuses as is required for Playwright.
|
||||||
|
prepare_cmd: |
|
||||||
|
hdiutil attach ./dist/*.dmg -mountpoint /Volumes/Element &&
|
||||||
|
rsync -a /Volumes/Element/Element*.app ~/Applications/ &&
|
||||||
|
hdiutil detach /Volumes/Element
|
||||||
|
blob_report: ${{ inputs.blob_report }}
|
||||||
189
.github/workflows/build_prepare.yaml
vendored
189
.github/workflows/build_prepare.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Prepare Build
|
# This action helps perform common actions before the build_* actions are started in parallel.
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -10,90 +10,163 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
required: false
|
required: false
|
||||||
description: "The version tag to fetch, or 'develop', will pick automatically if not passed"
|
description: "The version tag to fetch, or 'develop', will pick automatically if not passed"
|
||||||
|
nightly:
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
description: "Whether the build is a Nightly and to calculate the version strings new builds should use"
|
||||||
|
deploy:
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
description: "Whether the build should be deployed to production"
|
||||||
|
branch-matching:
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
description: "Whether the branch name should be matched to find the element-web commit"
|
||||||
|
secrets:
|
||||||
|
# Required if `nightly` is set
|
||||||
|
CF_R2_ACCESS_KEY_ID:
|
||||||
|
required: false
|
||||||
|
# Required if `nightly` is set
|
||||||
|
CF_R2_TOKEN:
|
||||||
|
required: false
|
||||||
outputs:
|
outputs:
|
||||||
|
nightly-version:
|
||||||
|
description: "The version string the next Nightly should use, only output for nightly"
|
||||||
|
value: ${{ jobs.prepare.outputs.nightly-version }}
|
||||||
packages-dir:
|
packages-dir:
|
||||||
description: "The directory non-deb packages for this run should live in"
|
description: "The directory non-deb packages for this run should live in within packages.element.io"
|
||||||
value: "desktop"
|
value: ${{ inputs.nightly && 'nightly' || 'desktop' }}
|
||||||
|
# This is just a simple pass-through of the input to simplify reuse of complex inline conditions
|
||||||
deploy:
|
deploy:
|
||||||
description: "Whether the build should be deployed to production"
|
description: "Whether the build should be deployed to production"
|
||||||
value: false
|
value: ${{ inputs.deploy }}
|
||||||
permissions: {}
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
name: Prepare
|
name: Prepare
|
||||||
runs-on: ubuntu-latest
|
environment: ${{ inputs.nightly && 'packages.element.io' || '' }}
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
secrets: read
|
outputs:
|
||||||
|
nightly-version: ${{ steps.versions.outputs.nightly }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: .node-version
|
node-version-file: .node-version
|
||||||
|
cache: "yarn"
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libnss3-dev
|
|
||||||
|
|
||||||
- name: Install Yarn
|
|
||||||
run: npm install -g yarn
|
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install --frozen-lockfile"
|
run: "yarn install --frozen-lockfile"
|
||||||
|
|
||||||
- name: Clone Element Web
|
- name: Fetch Element Web (matching branch)
|
||||||
|
id: branch-matching
|
||||||
|
if: inputs.branch-matching
|
||||||
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
git clone https://git.piskot.si/nikrozman/element-web.git element-web
|
scripts/branch-match.sh
|
||||||
cd element-web
|
cp "$CONFIG_DIR/config.json" element-web/
|
||||||
yarn install --frozen-lockfile
|
yarn --cwd element-web install --frozen-lockfile
|
||||||
yarn build
|
yarn --cwd element-web run build
|
||||||
cd ..
|
mv element-web/webapp .
|
||||||
yarn run asar pack element-web/webapp webapp.asar
|
yarn asar-webapp
|
||||||
|
env:
|
||||||
|
# These must be set for branch-match.sh to get the right branch
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
CONFIG_DIR: ${{ inputs.config }}
|
||||||
|
|
||||||
|
- name: Fetch Element Web (${{ inputs.version }})
|
||||||
|
if: steps.branch-matching.outcome == 'failure' || steps.branch-matching.outcome == 'skipped'
|
||||||
|
run: yarn run fetch --noverify -d ${{ inputs.config }} ${{ inputs.version }}
|
||||||
|
|
||||||
|
# We split this out to save the build_* scripts having to do it to make use of `hashFiles` in the cache action
|
||||||
- name: Generate cache hash files
|
- name: Generate cache hash files
|
||||||
run: |
|
run: |
|
||||||
# Save electron version
|
# Add --no-sandbox as otherwise it fails because the helper isn't setuid root. It's only getting the version.
|
||||||
NODE_ENV=production node -e "console.log(require('./package.json').devDependencies.electron)" > electronVersion
|
yarn run --silent electron --no-sandbox --version > electronVersion
|
||||||
|
cat package.json | jq -c .hakDependencies | sha1sum > hakHash
|
||||||
# Generate hak dependencies hash
|
find hak -type f -print0 | xargs -0 sha1sum >> hakHash
|
||||||
jq -c .hakDependencies package.json | sha1sum > hakHash
|
find scripts/hak -type f -print0 | xargs -0 sha1sum >> hakHash
|
||||||
if [ -d "hak" ]; then
|
|
||||||
find hak -type f -print0 | sort -z | xargs -0 sha1sum >> hakHash
|
|
||||||
fi
|
|
||||||
if [ -d "scripts/hak" ]; then
|
|
||||||
find scripts/hak -type f -print0 | sort -z | xargs -0 sha1sum >> hakHash
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
- name: "[Nightly] Calculate version"
|
||||||
- name: Setup MinIO client
|
id: versions
|
||||||
uses: yakubique/setup-minio-cli@v1
|
if: inputs.nightly
|
||||||
|
|
||||||
- name: Test credential
|
|
||||||
run: |
|
run: |
|
||||||
echo "${S3_AK:0:4}" "${S3_SK:0:4}"
|
# Find all latest Nightly versions
|
||||||
|
aws s3 cp s3://$R2_BUCKET/nightly/update/macos/releases.json - --endpoint-url $R2_URL --region auto | jq -r .currentRelease >> VERSIONS
|
||||||
|
aws s3 cp s3://$R2_BUCKET/debian/dists/default/main/binary-amd64/Packages - --endpoint-url $R2_URL --region auto | grep "Package: element-nightly" -A 50 | grep Version -m1 | sed -n 's/Version: //p' >> VERSIONS
|
||||||
|
aws s3 cp s3://$R2_BUCKET/debian/dists/default/main/binary-arm64/Packages - --endpoint-url $R2_URL --region auto | grep "Package: element-nightly" -A 50 | grep Version -m1 | sed -n 's/Version: //p' >> VERSIONS
|
||||||
|
aws s3 cp s3://$R2_BUCKET/nightly/update/win32/x64/RELEASES - --endpoint-url $R2_URL --region auto | awk '{print $2}' | cut -d "-" -f 5 | cut -c 8- >> VERSIONS
|
||||||
|
aws s3 cp s3://$R2_BUCKET/nightly/update/win32/arm64/RELEASES - --endpoint-url $R2_URL --region auto | awk '{print $2}' | cut -d "-" -f 5 | cut -c 8- >> VERSIONS
|
||||||
|
|
||||||
|
# Pick the greatest one
|
||||||
|
VERSION=$(cat VERSIONS | sort -uf | tail -n1)
|
||||||
|
echo "Found latest nightly version $VERSION"
|
||||||
|
# Increment it
|
||||||
|
echo "nightly=$(scripts/generate-nightly-version.ts --latest $VERSION)" >> $GITHUB_OUTPUT
|
||||||
env:
|
env:
|
||||||
S3_AK: ${{ secrets.S3_AK }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||||
S3_SK: ${{ secrets.S3_SK }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
||||||
|
R2_BUCKET: ${{ vars.R2_BUCKET }}
|
||||||
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
|
|
||||||
- name: Configure MinIO client
|
- name: Check version
|
||||||
|
id: package
|
||||||
run: |
|
run: |
|
||||||
mc alias set s3piskot https://s3.piskot.si "${S3_AK}" "${S3_SK}"
|
echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUT
|
||||||
if ! mc alias list | grep -q "s3piskot"; then
|
|
||||||
echo "Error: Failed to set S3 alias"
|
- name: "[Release] Fetch release"
|
||||||
exit 1
|
id: release
|
||||||
fi
|
if: ${{ !inputs.nightly && inputs.version != 'develop' }}
|
||||||
|
uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada # v1
|
||||||
echo "Successfully configured MinIO client"
|
|
||||||
env:
|
env:
|
||||||
S3_AK: ${{ secrets.S3_AK }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
S3_SK: ${{ secrets.S3_SK }}
|
with:
|
||||||
|
tag: v${{ steps.package.outputs.version }}
|
||||||
|
|
||||||
- name: Upload cache files
|
- name: "[Release] Write changelog"
|
||||||
|
if: ${{ !inputs.nightly && inputs.version != 'develop' }}
|
||||||
run: |
|
run: |
|
||||||
# Upload cache files to MinIO
|
TIME=$(date -d "$PUBLISHED_AT" -R)
|
||||||
mc cp webapp.asar s3piskot/element-desktop/staging/
|
echo "element-desktop ($VERSION) default; urgency=medium" >> changelog.Debian
|
||||||
mc cp package.json s3piskot/element-desktop/staging/
|
echo "$BODY" | sed 's/^##/\n */g;s/^\*/ */g' | perl -pe 's/\[.+?]\((.+?)\)/\1/g' >> changelog.Debian
|
||||||
mc cp electronVersion s3piskot/element-desktop/staging/
|
echo "" >> changelog.Debian
|
||||||
mc cp hakHash s3piskot/element-desktop/staging/
|
echo " -- $ACTOR <support@element.io> $TIME" >> changelog.Debian
|
||||||
|
env:
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
VERSION: v${{ steps.package.outputs.version }}
|
||||||
|
BODY: ${{ steps.release.outputs.body }}
|
||||||
|
PUBLISHED_AT: ${{ steps.release.outputs.published_at }}
|
||||||
|
|
||||||
|
- name: "[Nightly] Write summary"
|
||||||
|
if: inputs.nightly
|
||||||
|
run: |
|
||||||
|
BUNDLE_HASH=$(npx asar l webapp.asar | grep /bundles/ | head -n 1 | sed 's|.*/||')
|
||||||
|
WEBAPP_VERSION=$(./scripts/get-version.ts)
|
||||||
|
WEB_VERSION=${WEBAPP_VERSION:0:12}
|
||||||
|
JS_VERSION=${WEBAPP_VERSION:16:12}
|
||||||
|
|
||||||
|
echo "### Nightly build ${{ steps.versions.outputs.nightly }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Component | Version |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| ----------- | ------- |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Bundle Hash | $BUNDLE_HASH |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Element Web | [$WEB_VERSION](https://github.com/element-hq/element-web/commit/$WEB_VERSION) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| JS SDK | [$JS_VERSION](https://github.com/matrix-org/matrix-js-sdk/commit/$JS_VERSION) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: webapp
|
||||||
|
retention-days: 1
|
||||||
|
path: |
|
||||||
|
webapp.asar
|
||||||
|
package.json
|
||||||
|
electronVersion
|
||||||
|
hakHash
|
||||||
|
changelog.Debian
|
||||||
|
|||||||
12
.github/workflows/build_test.yaml
vendored
12
.github/workflows/build_test.yaml
vendored
@@ -27,9 +27,9 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ${{ inputs.runs-on }}
|
runs-on: ${{ inputs.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: .node-version
|
node-version-file: .node-version
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "yarn install --frozen-lockfile"
|
run: "yarn install --frozen-lockfile"
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
name: ${{ inputs.artifact }}
|
name: ${{ inputs.artifact }}
|
||||||
path: dist
|
path: dist
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a
|
uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a
|
||||||
timeout-minutes: 5
|
timeout-minutes: 20
|
||||||
with:
|
with:
|
||||||
run: yarn test --project=${{ inputs.artifact }} ${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} ${{ inputs.blob_report == false && '--reporter=html' || '' }}
|
run: yarn test --project=${{ inputs.artifact }} ${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} ${{ inputs.blob_report == false && '--reporter=html' || '' }}
|
||||||
env:
|
env:
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload blob report
|
- name: Upload blob report
|
||||||
if: always() && inputs.blob_report
|
if: always() && inputs.blob_report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: blob-report-${{ inputs.artifact }}
|
name: blob-report-${{ inputs.artifact }}
|
||||||
path: blob-report
|
path: blob-report
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload HTML report
|
- name: Upload HTML report
|
||||||
if: always() && inputs.blob_report == false
|
if: always() && inputs.blob_report == false
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: ${{ inputs.artifact }}-test
|
name: ${{ inputs.artifact }}-test
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
|
|||||||
10
.github/workflows/build_windows.yaml
vendored
10
.github/workflows/build_windows.yaml
vendored
@@ -65,15 +65,15 @@ jobs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
name: webapp
|
name: webapp
|
||||||
|
|
||||||
- name: Cache .hak
|
- name: Cache .hak
|
||||||
id: cache
|
id: cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||||
with:
|
with:
|
||||||
key: ${{ runner.os }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion') }}
|
key: ${{ runner.os }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion') }}
|
||||||
path: |
|
path: |
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
rustup default stable
|
rustup default stable
|
||||||
rustup target add ${{ steps.config.outputs.target }}
|
rustup target add ${{ steps.config.outputs.target }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: .node-version
|
node-version-file: .node-version
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
@@ -206,7 +206,7 @@ jobs:
|
|||||||
| ForEach-Object -Process {. $env:SIGNTOOL_PATH verify /pa $_.FullName; if(!$?) { throw }}
|
| ForEach-Object -Process {. $env:SIGNTOOL_PATH verify /pa $_.FullName; if(!$?) { throw }}
|
||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: win-${{ inputs.arch }}
|
name: win-${{ inputs.arch }}
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
70
.github/workflows/dockerbuild.yaml
vendored
Normal file
70
.github/workflows/dockerbuild.yaml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: Dockerbuild
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
push:
|
||||||
|
branches: [master, staging, develop]
|
||||||
|
paths:
|
||||||
|
- "dockerbuild/**"
|
||||||
|
pull_request:
|
||||||
|
concurrency: ${{ github.workflow }}-${{ github.ref_name }}
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}-dockerbuild
|
||||||
|
permissions: {}
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Docker Build
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||||
|
with:
|
||||||
|
install: true
|
||||||
|
|
||||||
|
- name: Build test image
|
||||||
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||||
|
with:
|
||||||
|
file: dockerbuild/Dockerfile
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: element-desktop-dockerbuild
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
- name: Test image
|
||||||
|
run: docker run -v $PWD:/project element-desktop-dockerbuild yarn install
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@6d4b68b490aef8836e8fb5e50ee7b3bdfa5894f0
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker
|
||||||
|
id: meta
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||||
|
with:
|
||||||
|
file: dockerbuild/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
85
.github/workflows/static_analysis.yaml
vendored
Normal file
85
.github/workflows/static_analysis.yaml
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
name: Static Analysis
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
push:
|
||||||
|
branches: [develop, master]
|
||||||
|
permissions: {} # No permissions needed
|
||||||
|
jobs:
|
||||||
|
ts_lint:
|
||||||
|
name: "Typescript Syntax Check"
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version-file: package.json
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
# Does not need branch matching as only analyses this layer
|
||||||
|
- name: Install Deps
|
||||||
|
run: "yarn install --frozen-lockfile"
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: "yarn run lint:types"
|
||||||
|
|
||||||
|
i18n_lint:
|
||||||
|
name: "i18n Check"
|
||||||
|
uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
with:
|
||||||
|
hardcoded-words: "Element"
|
||||||
|
|
||||||
|
js_lint:
|
||||||
|
name: "ESLint"
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version-file: package.json
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
# Does not need branch matching as only analyses this layer
|
||||||
|
- name: Install Deps
|
||||||
|
run: "yarn install --frozen-lockfile"
|
||||||
|
|
||||||
|
- name: Run Linter
|
||||||
|
run: "yarn run lint:js"
|
||||||
|
|
||||||
|
workflow_lint:
|
||||||
|
name: "Workflow Lint"
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version-file: package.json
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
# Does not need branch matching as only analyses this layer
|
||||||
|
- name: Install Deps
|
||||||
|
run: "yarn install --frozen-lockfile"
|
||||||
|
|
||||||
|
- name: Run Linter
|
||||||
|
run: "yarn lint:workflows"
|
||||||
|
|
||||||
|
analyse_dead_code:
|
||||||
|
name: "Analyse Dead Code"
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version-file: package.json
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Install Deps
|
||||||
|
run: "yarn install --frozen-lockfile"
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: "yarn run lint:knip"
|
||||||
22
.github/workflows/triage-stale.yml
vendored
Normal file
22
.github/workflows/triage-stale.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Close stale PRs
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
permissions: {}
|
||||||
|
jobs:
|
||||||
|
close:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||||
|
with:
|
||||||
|
operations-per-run: 250
|
||||||
|
days-before-issue-stale: -1
|
||||||
|
days-before-issue-close: -1
|
||||||
|
days-before-pr-stale: 180
|
||||||
|
days-before-pr-close: 0
|
||||||
|
close-pr-message: "This PR has been automatically closed because it has been stale for 180 days. If you wish to continue working on this PR, please ping a maintainer to reopen it."
|
||||||
@@ -1 +1 @@
|
|||||||
v22.14.0
|
v22.16.0
|
||||||
|
|||||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,3 +1,57 @@
|
|||||||
|
Changes in [1.11.101](https://github.com/element-hq/element-desktop/releases/tag/v1.11.101) (2025-05-20)
|
||||||
|
========================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* Migrate from keytar to safeStorage ([#2227](https://github.com/element-hq/element-desktop/pull/2227)). Contributed by @t3chguy.
|
||||||
|
* New room list: add keyboard navigation support ([#29805](https://github.com/element-hq/element-web/pull/29805)). Contributed by @florianduros.
|
||||||
|
* Use the JoinRuleSettings component for the guest link access prompt. ([#28614](https://github.com/element-hq/element-web/pull/28614)). Contributed by @toger5.
|
||||||
|
* Add loading state to the new room list view ([#29725](https://github.com/element-hq/element-web/pull/29725)). Contributed by @langleyd.
|
||||||
|
* Make OIDC identity reset consistent with EX ([#29854](https://github.com/element-hq/element-web/pull/29854)). Contributed by @andybalaam.
|
||||||
|
* Support error code for email / phone adding unsupported (MSC4178) ([#29855](https://github.com/element-hq/element-web/pull/29855)). Contributed by @dbkr.
|
||||||
|
* Update identity reset UI (Make consistent with EX) ([#29701](https://github.com/element-hq/element-web/pull/29701)). Contributed by @andybalaam.
|
||||||
|
* Add secondary filters to the new room list ([#29818](https://github.com/element-hq/element-web/pull/29818)). Contributed by @dbkr.
|
||||||
|
* Fix battery drain from Web Audio ([#29203](https://github.com/element-hq/element-web/pull/29203)). Contributed by @mbachry.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* Fix go home shortcut on macos and change toggle action events shortcut ([#29929](https://github.com/element-hq/element-web/pull/29929)). Contributed by @florianduros.
|
||||||
|
* New room list: fix outdated message preview when space or filter change ([#29925](https://github.com/element-hq/element-web/pull/29925)). Contributed by @florianduros.
|
||||||
|
* Stop migrating to MSC4278 if the config exists. ([#29924](https://github.com/element-hq/element-web/pull/29924)). Contributed by @Half-Shot.
|
||||||
|
* Ensure consistent download file name on download from ImageView ([#29913](https://github.com/element-hq/element-web/pull/29913)). Contributed by @t3chguy.
|
||||||
|
* Add error toast when service worker registration fails ([#29895](https://github.com/element-hq/element-web/pull/29895)). Contributed by @t3chguy.
|
||||||
|
* New Room List: Prevent old tombstoned rooms from appearing in the list ([#29881](https://github.com/element-hq/element-web/pull/29881)). Contributed by @MidhunSureshR.
|
||||||
|
* Remove lag in search field ([#29885](https://github.com/element-hq/element-web/pull/29885)). Contributed by @florianduros.
|
||||||
|
* Respect UIFeature.Voip ([#29873](https://github.com/element-hq/element-web/pull/29873)). Contributed by @langleyd.
|
||||||
|
* Allow jumping to message search from spotlight ([#29850](https://github.com/element-hq/element-web/pull/29850)). Contributed by @t3chguy.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [1.11.100](https://github.com/element-hq/element-desktop/releases/tag/v1.11.100) (2025-05-06)
|
||||||
|
========================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* Move rich topics out of labs / stabilise MSC3765 ([#29817](https://github.com/element-hq/element-web/pull/29817)). Contributed by @Johennes.
|
||||||
|
* Spell out that Element Web does \*not\* work on mobile. ([#29211](https://github.com/element-hq/element-web/pull/29211)). Contributed by @ara4n.
|
||||||
|
* Add message preview support to the new room list ([#29784](https://github.com/element-hq/element-web/pull/29784)). Contributed by @dbkr.
|
||||||
|
* Global configuration flag for media previews ([#29582](https://github.com/element-hq/element-web/pull/29582)). Contributed by @Half-Shot.
|
||||||
|
* New room list: add partial keyboard shortcuts support ([#29783](https://github.com/element-hq/element-web/pull/29783)). Contributed by @florianduros.
|
||||||
|
* MVVM RoomSummaryCard Topic ([#29710](https://github.com/element-hq/element-web/pull/29710)). Contributed by @MarcWadai.
|
||||||
|
* Warn on self change from settings > roles ([#28926](https://github.com/element-hq/element-web/pull/28926)). Contributed by @MarcWadai.
|
||||||
|
* New room list: new visual for invitation ([#29773](https://github.com/element-hq/element-web/pull/29773)). Contributed by @florianduros.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* Apply workaround to fix app launching on Linux ([#2308](https://github.com/element-hq/element-desktop/pull/2308)). Contributed by @dbkr.
|
||||||
|
* Notification fixes for Windows - AppID name was messing up handler ([#2275](https://github.com/element-hq/element-desktop/pull/2275)). Contributed by @Fusseldieb.
|
||||||
|
* Fix incorrect display of the user info display name ([#29826](https://github.com/element-hq/element-web/pull/29826)). Contributed by @langleyd.
|
||||||
|
* RoomListStore: Remove invite rooms on decline ([#29804](https://github.com/element-hq/element-web/pull/29804)). Contributed by @MidhunSureshR.
|
||||||
|
* Fix the buttons not being displayed with long preview text ([#29811](https://github.com/element-hq/element-web/pull/29811)). Contributed by @dbkr.
|
||||||
|
* New room list: fix missing/incorrect notification decoration ([#29796](https://github.com/element-hq/element-web/pull/29796)). Contributed by @florianduros.
|
||||||
|
* New Room List: Prevent potential scroll jump/flicker when switching spaces ([#29781](https://github.com/element-hq/element-web/pull/29781)). Contributed by @MidhunSureshR.
|
||||||
|
* New room list: fix incorrect decoration ([#29770](https://github.com/element-hq/element-web/pull/29770)). Contributed by @florianduros.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.99](https://github.com/element-hq/element-desktop/releases/tag/v1.11.99) (2025-04-23)
|
Changes in [1.11.99](https://github.com/element-hq/element-desktop/releases/tag/v1.11.99) (2025-04-23)
|
||||||
======================================================================================================
|
======================================================================================================
|
||||||
## 🐛 Bug Fixes
|
## 🐛 Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Docker image to facilitate building Element Desktop's native bits using a glibc version (2.31)
|
# Docker image to facilitate building Element Desktop's native bits using a glibc version (2.31)
|
||||||
# with broader compatibility, down to Debian bullseye & Ubuntu focal.
|
# with broader compatibility, down to Debian bullseye & Ubuntu focal.
|
||||||
FROM rust:bullseye
|
FROM rust:bullseye@sha256:eb809362961259a30f540857c3cac8423c466d558bea0f55f32e3a6354654353
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
- [Introduction](../README.md)
|
- [Introduction](../README.md)
|
||||||
|
|
||||||
# Build
|
# Build/Debug
|
||||||
|
|
||||||
- [Native Node modules](native-node-modules.md)
|
- [Native Node modules](native-node-modules.md)
|
||||||
- [Windows requirements](windows-requirements.md)
|
- [Windows requirements](windows-requirements.md)
|
||||||
|
- [Debugging](debugging.md)
|
||||||
|
- [Using gdb](gdb.md)
|
||||||
|
|
||||||
# Distribution
|
# Distribution
|
||||||
|
|
||||||
|
|||||||
28
docs/debugging.md
Normal file
28
docs/debugging.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Debugging Element-Desktop
|
||||||
|
|
||||||
|
There are two parts of the desktop app that you might want to debug.
|
||||||
|
|
||||||
|
## The renderer process
|
||||||
|
|
||||||
|
This is the regular element-web codeand can be debugged by just selecting 'toggle developer tools'
|
||||||
|
from the menu, even on ppackaged builds. This then works the same as chrome dev tools for element web.
|
||||||
|
|
||||||
|
## The main process
|
||||||
|
|
||||||
|
This is debugged as a node app, so:
|
||||||
|
|
||||||
|
1. Open any chrome dev tools window
|
||||||
|
1. Start element with the `--inspect-brk` flag
|
||||||
|
1. Notice that you now have a little green icon in the top left of your chrome devtools window, click it.
|
||||||
|
|
||||||
|
You are now debugging the code of the desktop app itself.
|
||||||
|
|
||||||
|
## The main process of a package app
|
||||||
|
|
||||||
|
When the app is shipped, electron's "fuses" are flipped, editing the electron binary itself to prevent certain features from being usable, one of which is debugging using `--inspect-brk` as above. You can flip the fuse back on Linux as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo npx @electron/fuses write --app /opt/Element/element-desktop EnableNodeCliInspectArguments=on
|
||||||
|
```
|
||||||
|
|
||||||
|
A similar command will work, in theory, on mac and windows, except that this will break code signing (which is the point of fuses) so you would have to re-sign the app or somesuch.
|
||||||
46
docs/gdb.md
Normal file
46
docs/gdb.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Using gdb against Element-Desktop
|
||||||
|
|
||||||
|
Occasionally it is useful to be able to connect to a running Element-Desktop
|
||||||
|
with [`gdb`](https://sourceware.org/gdb/), or to analayze a coredump. For this,
|
||||||
|
you will need debug symbols.
|
||||||
|
|
||||||
|
1. If you don't already have the right version of Element-Desktop (eg because
|
||||||
|
you are analyzing someone else's coredump), download and unpack the tarball
|
||||||
|
from https://packages.element.io/desktop/install/linux/. If it was a
|
||||||
|
nightly, your best bet may be to download the deb from
|
||||||
|
https://packages.element.io/debian/pool/main/e/element-nightly/ and unpack
|
||||||
|
it.
|
||||||
|
2. Figure out which version of Electron your Element-Desktop is based on. The
|
||||||
|
best way to do this is to figure out the version of Element-Desktop, then
|
||||||
|
look at
|
||||||
|
[`yarn.lock`](https://github.com/element-hq/element-desktop/blob/develop/yarn.lock)
|
||||||
|
for the corresponding version. There should be an entry starting
|
||||||
|
`electron@`, and under it a `version` line: this will tell you the version
|
||||||
|
of Electron that was used for that version of Element-Desktop.
|
||||||
|
|
||||||
|
3. Go to [Electron's releases page](https://github.com/electron/electron/releases/)
|
||||||
|
and find the version you just identified. Under "Assets", download
|
||||||
|
`electron-v<version>-linux-x64-debug.zip` (or, the -debug zip corresponding to your
|
||||||
|
architecture).
|
||||||
|
|
||||||
|
4. The debug zip has a structure like:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── debug
|
||||||
|
│ ├── chrome_crashpad_handler.debug
|
||||||
|
│ ├── electron.debug
|
||||||
|
│ ├── libEGL.so.debug
|
||||||
|
│ ├── libffmpeg.so.debug
|
||||||
|
│ ├── libGLESv2.so.debug
|
||||||
|
│ └── libvk_swiftshader.so.debug
|
||||||
|
├── LICENSE
|
||||||
|
├── LICENSES.chromium.html
|
||||||
|
└── version
|
||||||
|
```
|
||||||
|
|
||||||
|
Take all the contents of `debug`, and copy them into the Element-Desktop directory,
|
||||||
|
so that `electron.debug` is alongside the `element-desktop-nightly` executable.
|
||||||
|
|
||||||
|
5. You now have a thing you can gdb as normal, either as `gdb --args element-desktop-nightly`, or
|
||||||
|
`gdb element-desktop-nightly core`.
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import { type Configuration as BaseConfiguration, type Protocol } from "electron-builder";
|
||||||
import * as plist from "plist";
|
|
||||||
import { AfterPackContext, Arch, Configuration as BaseConfiguration, Platform } from "electron-builder";
|
|
||||||
import { computeData } from "app-builder-lib/out/asar/integrity";
|
|
||||||
import { readFile, writeFile } from "node:fs/promises";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This script has different outputs depending on your os platform.
|
* This script has different outputs depending on your os platform.
|
||||||
@@ -20,9 +16,13 @@ import { readFile, writeFile } from "node:fs/promises";
|
|||||||
* Passes $ED_DEBIAN_CHANGELOG to build.deb.fpm if specified
|
* Passes $ED_DEBIAN_CHANGELOG to build.deb.fpm if specified
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const DEFAULT_APP_ID = "im.riot.app";
|
||||||
const NIGHTLY_APP_ID = "im.riot.nightly";
|
const NIGHTLY_APP_ID = "im.riot.nightly";
|
||||||
const NIGHTLY_DEB_NAME = "element-nightly";
|
const NIGHTLY_DEB_NAME = "element-nightly";
|
||||||
|
|
||||||
|
const DEFAULT_PROTOCOL_SCHEME = "io.element.desktop";
|
||||||
|
const NIGHTLY_PROTOCOL_SCHEME = "io.element.nightly";
|
||||||
|
|
||||||
interface Pkg {
|
interface Pkg {
|
||||||
name: string;
|
name: string;
|
||||||
productName: string;
|
productName: string;
|
||||||
@@ -37,7 +37,11 @@ type Writable<T> = NonNullable<
|
|||||||
const pkg: Pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
const pkg: Pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
||||||
|
|
||||||
interface Configuration extends BaseConfiguration {
|
interface Configuration extends BaseConfiguration {
|
||||||
extraMetadata: Partial<Pick<Pkg, "version">> & Omit<Pkg, "version">;
|
extraMetadata: Partial<Pick<Pkg, "version">> &
|
||||||
|
Omit<Pkg, "version"> & {
|
||||||
|
electron_appId: string;
|
||||||
|
electron_protocol: string;
|
||||||
|
};
|
||||||
linux: BaseConfiguration["linux"];
|
linux: BaseConfiguration["linux"];
|
||||||
win: BaseConfiguration["win"];
|
win: BaseConfiguration["win"];
|
||||||
mac: BaseConfiguration["mac"];
|
mac: BaseConfiguration["mac"];
|
||||||
@@ -46,26 +50,6 @@ interface Configuration extends BaseConfiguration {
|
|||||||
} & BaseConfiguration["deb"];
|
} & BaseConfiguration["deb"];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function injectAsarIntegrity(context: AfterPackContext) {
|
|
||||||
const packager = context.packager;
|
|
||||||
|
|
||||||
// We only need to re-generate asar on universal Mac builds, due to https://github.com/electron/universal/issues/116
|
|
||||||
if (packager.platform !== Platform.MAC || context.arch !== Arch.universal) return;
|
|
||||||
|
|
||||||
const resourcesPath = packager.getResourcesDir(context.appOutDir);
|
|
||||||
const asarIntegrity = await computeData({
|
|
||||||
resourcesPath,
|
|
||||||
resourcesRelativePath: "Resources",
|
|
||||||
resourcesDestinationPath: resourcesPath,
|
|
||||||
extraResourceMatchers: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const plistPath = path.join(resourcesPath, "..", "Info.plist");
|
|
||||||
const data = plist.parse(await readFile(plistPath, "utf8")) as unknown as Writable<plist.PlistObject>;
|
|
||||||
data["ElectronAsarIntegrity"] = asarIntegrity as unknown as Writable<plist.PlistValue>;
|
|
||||||
await writeFile(plistPath, plist.build(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('electron-builder').Configuration}
|
* @type {import('electron-builder').Configuration}
|
||||||
* @see https://www.electron.build/configuration/configuration
|
* @see https://www.electron.build/configuration/configuration
|
||||||
@@ -74,7 +58,7 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
|||||||
// Make all fuses required to ensure they are all explicitly specified
|
// Make all fuses required to ensure they are all explicitly specified
|
||||||
electronFuses: Required<Configuration["electronFuses"]>;
|
electronFuses: Required<Configuration["electronFuses"]>;
|
||||||
} = {
|
} = {
|
||||||
appId: "im.riot.app",
|
appId: DEFAULT_APP_ID,
|
||||||
asarUnpack: "**/*.node",
|
asarUnpack: "**/*.node",
|
||||||
electronFuses: {
|
electronFuses: {
|
||||||
enableCookieEncryption: true,
|
enableCookieEncryption: true,
|
||||||
@@ -90,9 +74,6 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
|||||||
loadBrowserProcessSpecificV8Snapshot: false,
|
loadBrowserProcessSpecificV8Snapshot: false,
|
||||||
enableEmbeddedAsarIntegrityValidation: true,
|
enableEmbeddedAsarIntegrityValidation: true,
|
||||||
},
|
},
|
||||||
afterPack: async (context: AfterPackContext) => {
|
|
||||||
await injectAsarIntegrity(context);
|
|
||||||
},
|
|
||||||
files: [
|
files: [
|
||||||
"package.json",
|
"package.json",
|
||||||
{
|
{
|
||||||
@@ -112,6 +93,8 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
|||||||
name: pkg.name,
|
name: pkg.name,
|
||||||
productName: pkg.productName,
|
productName: pkg.productName,
|
||||||
description: pkg.description,
|
description: pkg.description,
|
||||||
|
electron_appId: DEFAULT_APP_ID,
|
||||||
|
electron_protocol: DEFAULT_PROTOCOL_SCHEME,
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
target: ["tar.gz", "deb"],
|
target: ["tar.gz", "deb"],
|
||||||
@@ -154,6 +137,7 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
|||||||
entitlements: "./build/entitlements.mac.plist",
|
entitlements: "./build/entitlements.mac.plist",
|
||||||
icon: "build/icons/icon.icns",
|
icon: "build/icons/icon.icns",
|
||||||
mergeASARs: true,
|
mergeASARs: true,
|
||||||
|
x64ArchFiles: "**/matrix-seshat/*.node", // hak already runs lipo
|
||||||
},
|
},
|
||||||
win: {
|
win: {
|
||||||
target: ["squirrel", "msi"],
|
target: ["squirrel", "msi"],
|
||||||
@@ -168,12 +152,10 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
|||||||
directories: {
|
directories: {
|
||||||
output: "dist",
|
output: "dist",
|
||||||
},
|
},
|
||||||
protocols: [
|
protocols: {
|
||||||
{
|
name: "element",
|
||||||
name: "element",
|
schemes: [DEFAULT_PROTOCOL_SCHEME, "element"],
|
||||||
schemes: ["io.element.desktop", "element"],
|
},
|
||||||
},
|
|
||||||
],
|
|
||||||
nativeRebuilder: "sequential",
|
nativeRebuilder: "sequential",
|
||||||
nodeGypRebuild: false,
|
nodeGypRebuild: false,
|
||||||
npmRebuild: true,
|
npmRebuild: true,
|
||||||
@@ -196,11 +178,12 @@ if (process.env.ED_SIGNTOOL_SUBJECT_NAME && process.env.ED_SIGNTOOL_THUMBPRINT)
|
|||||||
if (process.env.ED_NIGHTLY) {
|
if (process.env.ED_NIGHTLY) {
|
||||||
config.deb.fpm = []; // Clear the fpm as the breaks deb fields don't apply to nightly
|
config.deb.fpm = []; // Clear the fpm as the breaks deb fields don't apply to nightly
|
||||||
|
|
||||||
config.appId = NIGHTLY_APP_ID;
|
config.appId = config.extraMetadata.electron_appId = NIGHTLY_APP_ID;
|
||||||
config.extraMetadata.productName += " Nightly";
|
config.extraMetadata.productName += " Nightly";
|
||||||
config.extraMetadata.name += "-nightly";
|
config.extraMetadata.name += "-nightly";
|
||||||
config.extraMetadata.description += " (nightly unstable build)";
|
config.extraMetadata.description += " (nightly unstable build)";
|
||||||
config.deb.fpm.push("--name", NIGHTLY_DEB_NAME);
|
config.deb.fpm.push("--name", NIGHTLY_DEB_NAME);
|
||||||
|
(config.protocols as Protocol).schemes[0] = config.extraMetadata.electron_protocol = NIGHTLY_PROTOCOL_SCHEME;
|
||||||
|
|
||||||
let version = process.env.ED_NIGHTLY;
|
let version = process.env.ED_NIGHTLY;
|
||||||
if (os.platform() === "win32") {
|
if (os.platform() === "win32") {
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -3,7 +3,7 @@
|
|||||||
"productName": "Element Piskot",
|
"productName": "Element Piskot",
|
||||||
"main": "lib/electron-main.js",
|
"main": "lib/electron-main.js",
|
||||||
"exports": "./lib/electron-main.js",
|
"exports": "./lib/electron-main.js",
|
||||||
"version": "1.11.97-piskot",
|
"version": "1.11.101-piskot",
|
||||||
"description": "Element: the future of secure communication",
|
"description": "Element: the future of secure communication",
|
||||||
"author": "Element",
|
"author": "Element",
|
||||||
"homepage": "https://element.io",
|
"homepage": "https://element.io",
|
||||||
@@ -74,22 +74,22 @@
|
|||||||
"@babel/core": "^7.18.10",
|
"@babel/core": "^7.18.10",
|
||||||
"@babel/preset-env": "^7.18.10",
|
"@babel/preset-env": "^7.18.10",
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
"@electron/asar": "3.4.1",
|
"@electron/asar": "4.0.0",
|
||||||
"@playwright/test": "1.51.1",
|
"@playwright/test": "1.52.0",
|
||||||
"@stylistic/eslint-plugin": "^4.0.0",
|
"@stylistic/eslint-plugin": "^4.0.0",
|
||||||
"@types/auto-launch": "^5.0.1",
|
"@types/auto-launch": "^5.0.1",
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/minimist": "^1.2.1",
|
"@types/minimist": "^1.2.1",
|
||||||
"@types/node": "18.19.86",
|
"@types/node": "18.19.105",
|
||||||
"@types/pacote": "^11.1.1",
|
"@types/pacote": "^11.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"app-builder-lib": "26.0.12",
|
"app-builder-lib": "26.0.15",
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"detect-libc": "^2.0.0",
|
"detect-libc": "^2.0.0",
|
||||||
"electron": "35.1.5",
|
"electron": "36.3.2",
|
||||||
"electron-builder": "26.0.12",
|
"electron-builder": "26.0.15",
|
||||||
"electron-builder-squirrel-windows": "26.0.12",
|
"electron-builder-squirrel-windows": "26.0.15",
|
||||||
"electron-devtools-installer": "^4.0.0",
|
"electron-devtools-installer": "^4.0.0",
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.26.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
@@ -101,11 +101,10 @@
|
|||||||
"glob": "^11.0.0",
|
"glob": "^11.0.0",
|
||||||
"husky": "^9.1.6",
|
"husky": "^9.1.6",
|
||||||
"knip": "^5.0.0",
|
"knip": "^5.0.0",
|
||||||
"lint-staged": "^15.2.10",
|
"lint-staged": "^16.0.0",
|
||||||
"matrix-web-i18n": "^3.2.1",
|
"matrix-web-i18n": "^3.2.1",
|
||||||
"mkdirp": "^3.0.0",
|
"mkdirp": "^3.0.0",
|
||||||
"pacote": "^21.0.0",
|
"pacote": "^21.0.0",
|
||||||
"plist": "^3.1.0",
|
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"rimraf": "^6.0.0",
|
"rimraf": "^6.0.0",
|
||||||
"tar": "^7.0.0",
|
"tar": "^7.0.0",
|
||||||
@@ -116,7 +115,8 @@
|
|||||||
"matrix-seshat": "^4.0.1"
|
"matrix-seshat": "^4.0.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "18.19.86",
|
"@types/node": "18.19.105",
|
||||||
"config-file-ts": "0.2.8-rc1"
|
"config-file-ts": "0.2.8-rc1",
|
||||||
|
"node-abi": "4.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mcr.microsoft.com/playwright:v1.51.1-jammy
|
FROM mcr.microsoft.com/playwright:v1.52.0-jammy@sha256:ff2946177f0756c87482c0ef958b7cfbf389b92525ace78a1c9890281d0d60f4
|
||||||
|
|
||||||
WORKDIR /work/element-desktop
|
WORKDIR /work/element-desktop
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { platform } from "node:os";
|
import keytar from "keytar-forked";
|
||||||
|
|
||||||
import { test, expect } from "../../element-desktop-test.js";
|
import { test, expect } from "../../element-desktop-test.js";
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ declare global {
|
|||||||
supportsEventIndexing(): Promise<boolean>;
|
supportsEventIndexing(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
|
getPickleKey(userId: string, deviceId: string): Promise<string | null>;
|
||||||
createPickleKey(userId: string, deviceId: string): Promise<string | null>;
|
createPickleKey(userId: string, deviceId: string): Promise<string | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,14 +49,46 @@ test.describe("App launch", () => {
|
|||||||
).resolves.toBeTruthy();
|
).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should launch and render the welcome view successfully and support keytar", async ({ page }) => {
|
test.describe("safeStorage", () => {
|
||||||
test.skip(platform() === "linux", "This test does not yet support Linux");
|
const userId = "@user:server";
|
||||||
|
const deviceId = "ABCDEF";
|
||||||
|
|
||||||
await expect(
|
test("should be supported", async ({ page }) => {
|
||||||
page.evaluate<string | null>(async () => {
|
await expect(
|
||||||
return await window.mxPlatformPeg.get().createPickleKey("@user:server", "ABCDEF");
|
page.evaluate(
|
||||||
}),
|
([userId, deviceId]) => window.mxPlatformPeg.get().createPickleKey(userId, deviceId),
|
||||||
).resolves.not.toBeNull();
|
[userId, deviceId],
|
||||||
|
),
|
||||||
|
).resolves.not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("migrate from keytar", () => {
|
||||||
|
test.skip(
|
||||||
|
process.env.GITHUB_ACTIONS && ["linux", "darwin"].includes(process.platform),
|
||||||
|
"GitHub Actions hosted runner are not a compatible environment for this test",
|
||||||
|
);
|
||||||
|
|
||||||
|
const pickleKey = "DEADBEEF1234";
|
||||||
|
const keytarService = "element.io";
|
||||||
|
const keytarKey = `${userId}|${deviceId}`;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await keytar.setPassword(keytarService, keytarKey, pickleKey);
|
||||||
|
await expect(keytar.getPassword(keytarService, keytarKey)).resolves.toBe(pickleKey);
|
||||||
|
});
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await keytar.deletePassword(keytarService, keytarKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should migrate successfully", async ({ page }) => {
|
||||||
|
await expect(
|
||||||
|
page.evaluate(
|
||||||
|
([userId, deviceId]) => window.mxPlatformPeg.get().getPickleKey(userId, deviceId),
|
||||||
|
[userId, deviceId],
|
||||||
|
),
|
||||||
|
).resolves.toBe(pickleKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("--no-update", () => {
|
test.describe("--no-update", () => {
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ test.describe("OIDC Native", () => {
|
|||||||
page.evaluate<string>(() => {
|
page.evaluate<string>(() => {
|
||||||
return window.mxPlatformPeg.get().getOidcCallbackUrl().toString();
|
return window.mxPlatformPeg.get().getOidcCallbackUrl().toString();
|
||||||
}),
|
}),
|
||||||
).resolves.toBe("io.element.desktop:/vector/webapp/");
|
).resolves.toMatch(/io\.element\.(desktop|nightly):\/vector\/webapp\//);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,12 +62,21 @@ export const test = base.extend<Fixtures>({
|
|||||||
// eslint-disable-next-line no-empty-pattern
|
// eslint-disable-next-line no-empty-pattern
|
||||||
tmpDir: async ({}, use) => {
|
tmpDir: async ({}, use) => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "element-desktop-tests-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "element-desktop-tests-"));
|
||||||
console.log("Using temp profile directory: ", tmpDir);
|
|
||||||
await use(tmpDir);
|
await use(tmpDir);
|
||||||
await fs.rm(tmpDir, { recursive: true });
|
await fs.rm(tmpDir, { recursive: true });
|
||||||
},
|
},
|
||||||
app: async ({ tmpDir, extraEnv, extraArgs, stdout, stderr }, use) => {
|
app: async ({ tmpDir, extraEnv, extraArgs, stdout, stderr }, use) => {
|
||||||
const args = ["--profile-dir", tmpDir];
|
const args = ["--profile-dir", tmpDir, ...extraArgs];
|
||||||
|
|
||||||
|
if (process.env.GITHUB_ACTIONS) {
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
// GitHub Actions hosted runner lacks dbus and a compatible keyring, so we need to force plaintext storage
|
||||||
|
args.push("--storage-mode", "force-plaintext");
|
||||||
|
} else if (process.platform === "darwin") {
|
||||||
|
// GitHub Actions hosted runner has no working default keychain, so allow plaintext storage
|
||||||
|
args.push("--storage-mode", "allow-plaintext");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const executablePath = process.env["ELEMENT_DESKTOP_EXECUTABLE"];
|
const executablePath = process.env["ELEMENT_DESKTOP_EXECUTABLE"];
|
||||||
if (!executablePath) {
|
if (!executablePath) {
|
||||||
@@ -75,13 +84,15 @@ export const test = base.extend<Fixtures>({
|
|||||||
args.unshift(path.join(__dirname, "..", "lib", "electron-main.js"));
|
args.unshift(path.join(__dirname, "..", "lib", "electron-main.js"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Launching '${executablePath}' with args ${args.join(" ")}`);
|
||||||
|
|
||||||
const app = await electron.launch({
|
const app = await electron.launch({
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
...extraEnv,
|
...extraEnv,
|
||||||
},
|
},
|
||||||
executablePath,
|
executablePath,
|
||||||
args: [...args, ...extraArgs],
|
args,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.process().stdout.pipe(stdout).pipe(process.stdout);
|
app.process().stdout.pipe(stdout).pipe(process.stdout);
|
||||||
|
|||||||
48
scripts/branch-match.sh
Executable file
48
scripts/branch-match.sh
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script for downloading a branch of element-web matching the branch a PR is contributed from
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
deforg="element-hq"
|
||||||
|
defrepo="element-web"
|
||||||
|
|
||||||
|
# The PR_NUMBER variable must be set explicitly.
|
||||||
|
default_org_repo=${GITHUB_REPOSITORY:-"$deforg/$defrepo"}
|
||||||
|
PR_ORG=${PR_ORG:-${default_org_repo%%/*}}
|
||||||
|
PR_REPO=${PR_REPO:-${default_org_repo##*/}}
|
||||||
|
|
||||||
|
# A function that clones a branch of a repo based on the org, repo and branch
|
||||||
|
clone() {
|
||||||
|
org=$1
|
||||||
|
repo=$2
|
||||||
|
branch=$3
|
||||||
|
if [ -n "$branch" ]
|
||||||
|
then
|
||||||
|
echo "Trying to use $org/$repo#$branch"
|
||||||
|
# Disable auth prompts: https://serverfault.com/a/665959
|
||||||
|
GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Getting info about a PR with number $PR_NUMBER"
|
||||||
|
apiEndpoint="https://api.github.com/repos/$PR_ORG/$PR_REPO/pulls/$PR_NUMBER"
|
||||||
|
head=$(curl "$apiEndpoint" | jq -r '.head.label')
|
||||||
|
|
||||||
|
# for forks, $head will be in the format "fork:branch", so we split it by ":"
|
||||||
|
# into an array. On non-forks, this has the effect of splitting into a single
|
||||||
|
# element array given ":" shouldn't appear in the head - it'll just be the
|
||||||
|
# branch name. Based on the results, we clone.
|
||||||
|
BRANCH_ARRAY=(${head//:/ })
|
||||||
|
TRY_ORG=$deforg
|
||||||
|
TRY_BRANCH=${BRANCH_ARRAY[0]}
|
||||||
|
if [[ "$head" == *":"* ]]; then
|
||||||
|
# ... but only match that fork if it's a real fork
|
||||||
|
if [ "${BRANCH_ARRAY[0]}" != "$PR_ORG" ]; then
|
||||||
|
TRY_ORG=${BRANCH_ARRAY[0]}
|
||||||
|
fi
|
||||||
|
TRY_BRANCH=${BRANCH_ARRAY[1]}
|
||||||
|
fi
|
||||||
|
clone "$TRY_ORG" "$defrepo" "$TRY_BRANCH"
|
||||||
|
|
||||||
|
exit 1
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -x
|
|
||||||
|
|
||||||
deforg="$1"
|
|
||||||
defrepo="$2"
|
|
||||||
defbranch="$3"
|
|
||||||
|
|
||||||
[ -z "$defbranch" ] && defbranch="develop"
|
|
||||||
|
|
||||||
rm -r "$defrepo" || true
|
|
||||||
|
|
||||||
clone() {
|
|
||||||
org=$1
|
|
||||||
repo=$2
|
|
||||||
branch=$3
|
|
||||||
if [ -n "$branch" ]
|
|
||||||
then
|
|
||||||
echo "Trying to use $org/$repo#$branch"
|
|
||||||
# Disable auth prompts: https://serverfault.com/a/665959
|
|
||||||
GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try the PR author's branch in case it exists on the deps as well.
|
|
||||||
# If BUILDKITE_BRANCH is set, it will contain either:
|
|
||||||
# * "branch" when the author's branch and target branch are in the same repo
|
|
||||||
# * "author:branch" when the author's branch is in their fork
|
|
||||||
# We can split on `:` into an array to check.
|
|
||||||
BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ })
|
|
||||||
if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "1" ]]; then
|
|
||||||
clone $deforg $defrepo $BUILDKITE_BRANCH
|
|
||||||
elif [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
|
|
||||||
clone ${BUILDKITE_BRANCH_ARRAY[0]} $defrepo ${BUILDKITE_BRANCH_ARRAY[1]}
|
|
||||||
fi
|
|
||||||
# Try the target branch of the push or PR.
|
|
||||||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
|
||||||
# Try the current branch from Jenkins.
|
|
||||||
clone $deforg $defrepo `"echo $GIT_BRANCH" | sed -e 's/^origin\///'`
|
|
||||||
# Use the default branch as the last resort.
|
|
||||||
clone $deforg $defrepo $defbranch
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"module": "node16",
|
"module": "node16",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": ["es2020"]
|
"lib": ["es2021"]
|
||||||
},
|
},
|
||||||
"include": ["../src/@types", "./**/*.ts"]
|
"include": ["../src/@types", "./**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/@types/global.d.ts
vendored
13
src/@types/global.d.ts
vendored
@@ -7,30 +7,23 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { type BrowserWindow } from "electron";
|
import { type BrowserWindow } from "electron";
|
||||||
|
|
||||||
import type Store from "electron-store";
|
|
||||||
import type AutoLaunch from "auto-launch";
|
import type AutoLaunch from "auto-launch";
|
||||||
import { type AppLocalization } from "../language-helper.js";
|
import { type AppLocalization } from "../language-helper.js";
|
||||||
|
|
||||||
// global type extensions need to use var for whatever reason
|
// global type extensions need to use var for whatever reason
|
||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
declare global {
|
declare global {
|
||||||
|
type IConfigOptions = Record<string, any>;
|
||||||
|
|
||||||
var mainWindow: BrowserWindow | null;
|
var mainWindow: BrowserWindow | null;
|
||||||
var appQuitting: boolean;
|
var appQuitting: boolean;
|
||||||
var appLocalization: AppLocalization;
|
var appLocalization: AppLocalization;
|
||||||
var launcher: AutoLaunch;
|
var launcher: AutoLaunch;
|
||||||
var vectorConfig: Record<string, any>;
|
var vectorConfig: IConfigOptions;
|
||||||
var trayConfig: {
|
var trayConfig: {
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
icon_path: string;
|
icon_path: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
};
|
};
|
||||||
var store: Store<{
|
|
||||||
warnBeforeExit?: boolean;
|
|
||||||
minimizeToTray?: boolean;
|
|
||||||
spellCheckerEnabled?: boolean;
|
|
||||||
autoHideMenuBar?: boolean;
|
|
||||||
locale?: string | string[];
|
|
||||||
disableHardwareAcceleration?: boolean;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
/* eslint-enable no-var */
|
/* eslint-enable no-var */
|
||||||
|
|||||||
26
src/build-config.ts
Normal file
26
src/build-config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path, { dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import { type JsonObject, loadJsonFile } from "./utils.js";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
interface BuildConfig {
|
||||||
|
appId: string;
|
||||||
|
protocol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readBuildConfig(): BuildConfig {
|
||||||
|
const packageJson = loadJsonFile(path.join(__dirname, "..", "package.json")) as JsonObject;
|
||||||
|
return {
|
||||||
|
appId: (packageJson["electron_appId"] as string) || "im.riot.app",
|
||||||
|
protocol: (packageJson["electron_protocol"] as string) || "io.element.desktop",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,13 +10,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
// Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
|
// Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
|
||||||
import "./squirrelhooks.js";
|
import "./squirrelhooks.js";
|
||||||
import { app, BrowserWindow, Menu, autoUpdater, protocol, dialog, type Input, type Event, session } from "electron";
|
import { app, BrowserWindow, Menu, autoUpdater, dialog, type Input, type Event, session, protocol } from "electron";
|
||||||
// eslint-disable-next-line n/file-extension-in-import
|
// eslint-disable-next-line n/file-extension-in-import
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import AutoLaunch from "auto-launch";
|
import AutoLaunch from "auto-launch";
|
||||||
import path, { dirname } from "node:path";
|
import path, { dirname } from "node:path";
|
||||||
import windowStateKeeper from "electron-window-state";
|
import windowStateKeeper from "electron-window-state";
|
||||||
import Store from "electron-store";
|
|
||||||
import fs, { promises as afs } from "node:fs";
|
import fs, { promises as afs } from "node:fs";
|
||||||
import { URL, fileURLToPath } from "node:url";
|
import { URL, fileURLToPath } from "node:url";
|
||||||
import minimist from "minimist";
|
import minimist from "minimist";
|
||||||
@@ -25,15 +24,17 @@ import "./ipc.js";
|
|||||||
import "./seshat.js";
|
import "./seshat.js";
|
||||||
import "./settings.js";
|
import "./settings.js";
|
||||||
import * as tray from "./tray.js";
|
import * as tray from "./tray.js";
|
||||||
|
import Store from "./store.js";
|
||||||
import { buildMenuTemplate } from "./vectormenu.js";
|
import { buildMenuTemplate } from "./vectormenu.js";
|
||||||
import webContentsHandler from "./webcontents-handler.js";
|
import webContentsHandler from "./webcontents-handler.js";
|
||||||
import * as updater from "./updater.js";
|
import * as updater from "./updater.js";
|
||||||
import { getProfileFromDeeplink, protocolInit } from "./protocol.js";
|
import ProtocolHandler from "./protocol.js";
|
||||||
import { _t, AppLocalization } from "./language-helper.js";
|
import { _t, AppLocalization } from "./language-helper.js";
|
||||||
import { setDisplayMediaCallback } from "./displayMediaCallback.js";
|
import { setDisplayMediaCallback } from "./displayMediaCallback.js";
|
||||||
import { setupMacosTitleBar } from "./macos-titlebar.js";
|
import { setupMacosTitleBar } from "./macos-titlebar.js";
|
||||||
import { type Json, loadJsonFile } from "./utils.js";
|
import { type Json, loadJsonFile } from "./utils.js";
|
||||||
import { setupMediaAuth } from "./media-auth.js";
|
import { setupMediaAuth } from "./media-auth.js";
|
||||||
|
import { readBuildConfig } from "./build-config.js";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -72,10 +73,13 @@ function isRealUserDataDir(d: string): boolean {
|
|||||||
return fs.existsSync(path.join(d, "IndexedDB"));
|
return fs.existsSync(path.join(d, "IndexedDB"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildConfig = readBuildConfig();
|
||||||
|
const protocolHandler = new ProtocolHandler(buildConfig.protocol);
|
||||||
|
|
||||||
// check if we are passed a profile in the SSO callback url
|
// check if we are passed a profile in the SSO callback url
|
||||||
let userDataPath: string;
|
let userDataPath: string;
|
||||||
|
|
||||||
const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]);
|
const userDataPathInProtocol = protocolHandler.getProfileFromDeeplink(argv["_"]);
|
||||||
if (userDataPathInProtocol) {
|
if (userDataPathInProtocol) {
|
||||||
userDataPath = userDataPathInProtocol;
|
userDataPath = userDataPathInProtocol;
|
||||||
} else if (argv["profile-dir"]) {
|
} else if (argv["profile-dir"]) {
|
||||||
@@ -273,8 +277,6 @@ async function moveAutoLauncher(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
global.store = new Store({ name: "electron-config" });
|
|
||||||
|
|
||||||
global.appQuitting = false;
|
global.appQuitting = false;
|
||||||
|
|
||||||
const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
|
const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
|
||||||
@@ -284,32 +286,6 @@ const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
|
|||||||
platform === "darwin" && input.meta && !input.control && input.key.toUpperCase() === "Q",
|
platform === "darwin" && input.meta && !input.control && input.key.toUpperCase() === "Q",
|
||||||
];
|
];
|
||||||
|
|
||||||
const warnBeforeExit = (event: Event, input: Input): void => {
|
|
||||||
const shouldWarnBeforeExit = global.store.get("warnBeforeExit", true);
|
|
||||||
const exitShortcutPressed =
|
|
||||||
input.type === "keyDown" && exitShortcuts.some((shortcutFn) => shortcutFn(input, process.platform));
|
|
||||||
|
|
||||||
if (shouldWarnBeforeExit && exitShortcutPressed && global.mainWindow) {
|
|
||||||
const shouldCancelCloseRequest =
|
|
||||||
dialog.showMessageBoxSync(global.mainWindow, {
|
|
||||||
type: "question",
|
|
||||||
buttons: [
|
|
||||||
_t("action|cancel"),
|
|
||||||
_t("action|close_brand", {
|
|
||||||
brand: global.vectorConfig.brand || "Element",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
message: _t("confirm_quit"),
|
|
||||||
defaultId: 1,
|
|
||||||
cancelId: 0,
|
|
||||||
}) === 0;
|
|
||||||
|
|
||||||
if (shouldCancelCloseRequest) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void configureSentry();
|
void configureSentry();
|
||||||
|
|
||||||
// handle uncaught errors otherwise it displays
|
// handle uncaught errors otherwise it displays
|
||||||
@@ -326,6 +302,11 @@ app.commandLine.appendSwitch("--enable-usermedia-screen-capturing");
|
|||||||
if (!app.commandLine.hasSwitch("enable-features")) {
|
if (!app.commandLine.hasSwitch("enable-features")) {
|
||||||
app.commandLine.appendSwitch("enable-features", "WebRTCPipeWireCapturer");
|
app.commandLine.appendSwitch("enable-features", "WebRTCPipeWireCapturer");
|
||||||
}
|
}
|
||||||
|
// Workaround bug in electron 36:https://github.com/electron/electron/issues/46538
|
||||||
|
// Hopefully this will no longer be needed soon and can be removed
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
app.commandLine.appendSwitch("gtk-version", "3");
|
||||||
|
}
|
||||||
|
|
||||||
const gotLock = app.requestSingleInstanceLock();
|
const gotLock = app.requestSingleInstanceLock();
|
||||||
if (!gotLock) {
|
if (!gotLock) {
|
||||||
@@ -334,7 +315,7 @@ if (!gotLock) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do this after we know we are the primary instance of the app
|
// do this after we know we are the primary instance of the app
|
||||||
protocolInit();
|
protocolHandler.initialise(userDataPath);
|
||||||
|
|
||||||
// Register the scheme the app is served from as 'standard'
|
// Register the scheme the app is served from as 'standard'
|
||||||
// which allows things like relative URLs and IndexedDB to
|
// which allows things like relative URLs and IndexedDB to
|
||||||
@@ -366,13 +347,17 @@ app.enableSandbox();
|
|||||||
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
|
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
|
||||||
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
|
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
|
||||||
|
|
||||||
|
const store = Store.initialize(argv["storage-mode"]); // must be called before any async actions
|
||||||
|
|
||||||
// Disable hardware acceleration if the setting has been set.
|
// Disable hardware acceleration if the setting has been set.
|
||||||
if (global.store.get("disableHardwareAcceleration", false) === true) {
|
if (store.get("disableHardwareAcceleration")) {
|
||||||
console.log("Disabling hardware acceleration.");
|
console.log("Disabling hardware acceleration.");
|
||||||
app.disableHardwareAcceleration();
|
app.disableHardwareAcceleration();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on("ready", async () => {
|
app.on("ready", async () => {
|
||||||
|
console.debug("Reached Electron ready state");
|
||||||
|
|
||||||
let asarPath: string;
|
let asarPath: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -462,12 +447,27 @@ app.on("ready", async () => {
|
|||||||
console.log("No update_base_url is defined: auto update is disabled");
|
console.log("No update_base_url is defined: auto update is disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up i18n before loading storage as we need translations for dialogs
|
||||||
|
global.appLocalization = new AppLocalization({
|
||||||
|
components: [(): void => tray.initApplicationMenu(), (): void => Menu.setApplicationMenu(buildMenuTemplate())],
|
||||||
|
store,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.debug("Ensuring storage is ready");
|
||||||
|
await store.safeStorageReady();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
app.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Load the previous window state with fallback to defaults
|
// Load the previous window state with fallback to defaults
|
||||||
const mainWindowState = windowStateKeeper({
|
const mainWindowState = windowStateKeeper({
|
||||||
defaultWidth: 1024,
|
defaultWidth: 1024,
|
||||||
defaultHeight: 768,
|
defaultHeight: 768,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.debug("Opening main window");
|
||||||
const preloadScript = path.normalize(`${__dirname}/preload.cjs`);
|
const preloadScript = path.normalize(`${__dirname}/preload.cjs`);
|
||||||
global.mainWindow = new BrowserWindow({
|
global.mainWindow = new BrowserWindow({
|
||||||
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||||
@@ -478,7 +478,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
icon: global.trayConfig.icon_path,
|
icon: global.trayConfig.icon_path,
|
||||||
show: false,
|
show: false,
|
||||||
autoHideMenuBar: global.store.get("autoHideMenuBar", true),
|
autoHideMenuBar: store.get("autoHideMenuBar"),
|
||||||
|
|
||||||
x: mainWindowState.x,
|
x: mainWindowState.x,
|
||||||
y: mainWindowState.y,
|
y: mainWindowState.y,
|
||||||
@@ -500,10 +500,10 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
// Handle spellchecker
|
// Handle spellchecker
|
||||||
// For some reason spellCheckerEnabled isn't persisted, so we have to use the store here
|
// For some reason spellCheckerEnabled isn't persisted, so we have to use the store here
|
||||||
global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true));
|
global.mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true));
|
||||||
|
|
||||||
// Create trayIcon icon
|
// Create trayIcon icon
|
||||||
if (global.store.get("minimizeToTray", true)) tray.create(global.trayConfig);
|
if (store.get("minimizeToTray")) tray.create(global.trayConfig);
|
||||||
|
|
||||||
global.mainWindow.once("ready-to-show", () => {
|
global.mainWindow.once("ready-to-show", () => {
|
||||||
if (!global.mainWindow) return;
|
if (!global.mainWindow) return;
|
||||||
@@ -517,7 +517,31 @@ app.on("ready", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
global.mainWindow.webContents.on("before-input-event", warnBeforeExit);
|
global.mainWindow.webContents.on("before-input-event", (event: Event, input: Input): void => {
|
||||||
|
const shouldWarnBeforeExit = store.get("warnBeforeExit", true);
|
||||||
|
const exitShortcutPressed =
|
||||||
|
input.type === "keyDown" && exitShortcuts.some((shortcutFn) => shortcutFn(input, process.platform));
|
||||||
|
|
||||||
|
if (shouldWarnBeforeExit && exitShortcutPressed && global.mainWindow) {
|
||||||
|
const shouldCancelCloseRequest =
|
||||||
|
dialog.showMessageBoxSync(global.mainWindow, {
|
||||||
|
type: "question",
|
||||||
|
buttons: [
|
||||||
|
_t("action|cancel"),
|
||||||
|
_t("action|close_brand", {
|
||||||
|
brand: global.vectorConfig.brand || "Element",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
message: _t("confirm_quit"),
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 0,
|
||||||
|
}) === 0;
|
||||||
|
|
||||||
|
if (shouldCancelCloseRequest) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
global.mainWindow.on("closed", () => {
|
global.mainWindow.on("closed", () => {
|
||||||
global.mainWindow = null;
|
global.mainWindow = null;
|
||||||
@@ -555,11 +579,6 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
webContentsHandler(global.mainWindow.webContents);
|
webContentsHandler(global.mainWindow.webContents);
|
||||||
|
|
||||||
global.appLocalization = new AppLocalization({
|
|
||||||
store: global.store,
|
|
||||||
components: [(): void => tray.initApplicationMenu(), (): void => Menu.setApplicationMenu(buildMenuTemplate())],
|
|
||||||
});
|
|
||||||
|
|
||||||
session.defaultSession.setDisplayMediaRequestHandler((_, callback) => {
|
session.defaultSession.setDisplayMediaRequestHandler((_, callback) => {
|
||||||
global.mainWindow?.webContents.send("openDesktopCapturerSourcePicker");
|
global.mainWindow?.webContents.send("openDesktopCapturerSourcePicker");
|
||||||
setDisplayMediaCallback(callback);
|
setDisplayMediaCallback(callback);
|
||||||
@@ -596,8 +615,9 @@ app.on("second-instance", (ev, commandLine, workingDirectory) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the App User Model ID to match what the squirrel
|
// This is required to make notification handlers work
|
||||||
// installer uses for the shortcut icon.
|
// on Windows 8.1/10/11 (and is a noop on other platforms);
|
||||||
// This makes notifications work on windows 8.1 (and is
|
// It must also match the ID found in 'electron-builder'
|
||||||
// a noop on other platforms).
|
// in order to get the title and icon to show up correctly.
|
||||||
app.setAppUserModelId("com.squirrel.element-desktop.Element");
|
// Ref: https://stackoverflow.com/a/77314604/3525780
|
||||||
|
app.setAppUserModelId(buildConfig.appId);
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "O",
|
"about": "O",
|
||||||
"brand_help": "%(brand)s nápověda",
|
"brand_help": "%(brand)s nápověda",
|
||||||
"help": "Nápověda",
|
"help": "Nápověda",
|
||||||
"preferences": "Předvolby"
|
"no": "Ne",
|
||||||
|
"preferences": "Předvolby",
|
||||||
|
"yes": "Ano"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Opravdu chcete ukončit aplikaci?",
|
"confirm_quit": "Opravdu chcete ukončit aplikaci?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,21 @@
|
|||||||
"save_image_as_error_description": "Obrázek se nepodařilo uložit",
|
"save_image_as_error_description": "Obrázek se nepodařilo uložit",
|
||||||
"save_image_as_error_title": "Chyba při ukládání obrázku"
|
"save_image_as_error_title": "Chyba při ukládání obrázku"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Vymazat data a znovu načíst?",
|
||||||
|
"backend_changed_detail": "Nelze získat přístup k tajnému klíči ze systémové klíčenky, zdá se, že se změnil.",
|
||||||
|
"backend_changed_title": "Nepodařilo se načíst databázi",
|
||||||
|
"backend_no_encryption": "Váš systém má podporovanou klíčenku, ale šifrování není k dispozici.",
|
||||||
|
"backend_no_encryption_detail": "Electron zjistil, že pro vaši klíčenku %(backend)s není k dispozici šifrování. Ujistěte se, že máte nainstalovanou klíčenku. Pokud ji máte, restartujte počítač a zkuste to znovu. Volitelně můžete povolit %(brand)s použít slabší formu šifrování.",
|
||||||
|
"backend_no_encryption_title": "Bez podpory šifrování",
|
||||||
|
"unsupported_keyring": "Váš systém má nepodporovanou klíčenku, což znamená, že databázi nelze otevřít.",
|
||||||
|
"unsupported_keyring_detail": "Detekce klíčenky Electronu nenalezla podporovaný backend. Můžete se pokusit ručně nakonfigurovat backend spuštěním %(brand)s s argumentem příkazového řádku, jednorázovou operací. Viz %(link)s.",
|
||||||
|
"unsupported_keyring_title": "Systém není podporován",
|
||||||
|
"unsupported_keyring_use_basic_text": "Používat slabší šifrování",
|
||||||
|
"unsupported_keyring_use_plaintext": "Nepoužívat žádné šifrování"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Aktuální velikost",
|
"actual_size": "Aktuální velikost",
|
||||||
"toggle_developer_tools": "Přepnout zobrazení nástrojů pro vývojáře",
|
"toggle_developer_tools": "Přepnout zobrazení nástrojů pro vývojáře",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Ynghylch",
|
"about": "Ynghylch",
|
||||||
"brand_help": "Cymorth %(brand)s",
|
"brand_help": "Cymorth %(brand)s",
|
||||||
"help": "Cymorth",
|
"help": "Cymorth",
|
||||||
"preferences": "Dewisiadau"
|
"no": "Na",
|
||||||
|
"preferences": "Dewisiadau",
|
||||||
|
"yes": "Iawn"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Ydych chi'n siŵr eich bod am roi'r gorau iddi?",
|
"confirm_quit": "Ydych chi'n siŵr eich bod am roi'r gorau iddi?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,19 @@
|
|||||||
"save_image_as_error_description": "Methodd cadw'r ddelwedd",
|
"save_image_as_error_description": "Methodd cadw'r ddelwedd",
|
||||||
"save_image_as_error_title": "Methodd cadw'r ddelwedd"
|
"save_image_as_error_title": "Methodd cadw'r ddelwedd"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Clirio data ac ail-lwytho?",
|
||||||
|
"backend_changed_detail": "Methu cael mynediad at y gyfrinach o allweddi'r system, mae'n ymddangos ei fod wedi newid.",
|
||||||
|
"backend_changed_title": "Methwyd llwytho'r gronfa ddata",
|
||||||
|
"backend_no_encryption": "Mae gan eich system cylch allwedd sy'n cael ei gefnogi ond nid yw amgryptio ar gael.",
|
||||||
|
"backend_no_encryption_detail": "Mae Electron wedi canfod nad yw amgryptio ar gael ar eich cylch allweddi %(backend)s. Gwnewch yn siŵr bod y cylch allweddi wedi'i osod. Os oes y cylch allweddi wedi'i osod, ail gychwynnwch a cheisiwch eto. Yn ddewisol, gallwch ganiatáu i %(brand)s ddefnyddio ffurf wannach o amgryptio.",
|
||||||
|
"backend_no_encryption_title": "Dim cefnogaeth amgryptio",
|
||||||
|
"unsupported_keyring": "Mae gan eich system allweddell nad yw'n cael ei chefnogi sy'n golygu nad oes modd agor y gronfa ddata.",
|
||||||
|
"unsupported_keyring_detail": "Heb ganfod allweddell Electron gefn. Gallwch geisio ffurfweddu'r gefn â llaw trwy gychwyn %(brand)s gyda dadl llinell orchymyn, gweithrediad untro. Gweler %(link)s.",
|
||||||
|
"unsupported_keyring_title": "System heb ei chefnogi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Maint Gwirioneddol",
|
"actual_size": "Maint Gwirioneddol",
|
||||||
"toggle_developer_tools": "Toglo Offer Datblygwyr",
|
"toggle_developer_tools": "Toglo Offer Datblygwyr",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Über",
|
"about": "Über",
|
||||||
"brand_help": "%(brand)s Hilfe",
|
"brand_help": "%(brand)s Hilfe",
|
||||||
"help": "Hilfe",
|
"help": "Hilfe",
|
||||||
"preferences": "Präferenzen"
|
"no": "Nein",
|
||||||
|
"preferences": "Präferenzen",
|
||||||
|
"yes": "Ja"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Wirklich beenden?",
|
"confirm_quit": "Wirklich beenden?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,19 @@
|
|||||||
"save_image_as_error_description": "Das Bild konnte nicht gespeichert werden",
|
"save_image_as_error_description": "Das Bild konnte nicht gespeichert werden",
|
||||||
"save_image_as_error_title": "Bild kann nicht gespeichert werden"
|
"save_image_as_error_title": "Bild kann nicht gespeichert werden"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Daten löschen und neu laden?",
|
||||||
|
"backend_changed_detail": "Zugriff auf Schlüssel im Systemschlüsselbund nicht möglich, er scheint sich geändert zu haben.",
|
||||||
|
"backend_changed_title": "Datenbank konnte nicht geladen werden",
|
||||||
|
"backend_no_encryption": "Ihr System verfügt über einen unterstützten Keyring, aber die Verschlüsselung ist nicht verfügbar.",
|
||||||
|
"backend_no_encryption_detail": "Electron hat festgestellt, dass Verschlüsselung in Ihrem Keyring %(backend)s nicht verfügbar ist. Bitte stellen Sie sicher, dass Sie den Keyringinstalliert haben. Wenn Sie den Keyring installiert haben, starten Sie ihn bitte neu und versuchen Sie es erneut. Optional können Sie die Verwendung einer schwächeren Form der Verschlüsselung zulassen %(brand)s.",
|
||||||
|
"backend_no_encryption_title": "Keine Verschlüsselungsunterstützung",
|
||||||
|
"unsupported_keyring": "Der Schlüsselbund ihres Systems wird nicht unterstützt, wodurch die Datenbank nicht geöffnet werden kann.",
|
||||||
|
"unsupported_keyring_detail": "Die Schlüsselbunderkennung von Electron hat kein unterstütztes Backend gefunden. Möglicherweise können sie dennoch den ihres Systemes verwenden. Infos unter %(link)s.",
|
||||||
|
"unsupported_keyring_title": "System nicht unterstützt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Tatsächliche Größe",
|
"actual_size": "Tatsächliche Größe",
|
||||||
"toggle_developer_tools": "Developer-Tools an/aus",
|
"toggle_developer_tools": "Developer-Tools an/aus",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "About",
|
"about": "About",
|
||||||
"brand_help": "%(brand)s Help",
|
"brand_help": "%(brand)s Help",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"preferences": "Preferences"
|
"no": "No",
|
||||||
|
"preferences": "Preferences",
|
||||||
|
"yes": "Yes"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Are you sure you want to quit?",
|
"confirm_quit": "Are you sure you want to quit?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,21 @@
|
|||||||
"save_image_as_error_description": "The image failed to save",
|
"save_image_as_error_description": "The image failed to save",
|
||||||
"save_image_as_error_title": "Failed to save image"
|
"save_image_as_error_title": "Failed to save image"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Clear data and reload?",
|
||||||
|
"backend_changed_detail": "Unable to access secret from system keyring, it appears to have changed.",
|
||||||
|
"backend_changed_title": "Failed to load database",
|
||||||
|
"backend_no_encryption": "Your system has a supported keyring but encryption is not available.",
|
||||||
|
"backend_no_encryption_detail": "Electron has detected that encryption is not available on your keyring %(backend)s. Please ensure that you have the keyring installed. If you do have the keyring installed, please reboot and try again. Optionally, you can allow %(brand)s to use a weaker form of encryption.",
|
||||||
|
"backend_no_encryption_title": "No encryption support",
|
||||||
|
"unsupported_keyring": "Your system has an unsupported keyring meaning the database cannot be opened.",
|
||||||
|
"unsupported_keyring_detail": "Electron's keyring detection did not find a supported backend. You can attempt to manually configure the backend by starting %(brand)s with a command-line argument, a one-time operation. See %(link)s.",
|
||||||
|
"unsupported_keyring_title": "System unsupported",
|
||||||
|
"unsupported_keyring_use_basic_text": "Use weaker encryption",
|
||||||
|
"unsupported_keyring_use_plaintext": "Use no encryption"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Actual Size",
|
"actual_size": "Actual Size",
|
||||||
"toggle_developer_tools": "Toggle Developer Tools",
|
"toggle_developer_tools": "Toggle Developer Tools",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Rakenduse teave",
|
"about": "Rakenduse teave",
|
||||||
"brand_help": "%(brand)s abiteave",
|
"brand_help": "%(brand)s abiteave",
|
||||||
"help": "Abiteave",
|
"help": "Abiteave",
|
||||||
"preferences": "Eelistused"
|
"no": "Ei",
|
||||||
|
"preferences": "Eelistused",
|
||||||
|
"yes": "Jah"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Kas sa kindlasti soovid rakendusest väljuda?",
|
"confirm_quit": "Kas sa kindlasti soovid rakendusest väljuda?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,21 @@
|
|||||||
"save_image_as_error_description": "Seda pilti ei õnnestunud salvestada",
|
"save_image_as_error_description": "Seda pilti ei õnnestunud salvestada",
|
||||||
"save_image_as_error_title": "Pildi salvestamine ei õnnestunud"
|
"save_image_as_error_title": "Pildi salvestamine ei õnnestunud"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Kas kustutame andmed ja laadime uuesti?",
|
||||||
|
"backend_changed_detail": "Süsteemsest võtmerõngast ei õnnestu laadida vajalikku saladust, tundub et ta on muutunud.",
|
||||||
|
"backend_changed_title": "Andmebaasi ei õnnestunud laadida",
|
||||||
|
"backend_no_encryption": "Sinu süsteem kasutab toetatud võtmerõngast, kuid krüptimist pole saadaval.",
|
||||||
|
"backend_no_encryption_detail": "Electron on tuvastanud, et krüptimine pole sinu %(backend)s võtmerõnga jaoks saadaval. Palun kontrolli, et võtmerõngas oleks korrektselt paigaldatud. Kui sul on võtmerõngas paigaldatud, siis palun taaskäivita ta ja proovi uuesti. Lisavõimalusena saad lubada, et %(brand)s kasutab nõrgemat krüptimislahendust.",
|
||||||
|
"backend_no_encryption_title": "Krüptimise tugi puudub",
|
||||||
|
"unsupported_keyring": "Sinu süsteemis on kasutusel mittetoetatud võtmerõnga versioon ning see tähendab, et andmebaasi ei saa avada.",
|
||||||
|
"unsupported_keyring_detail": "Electroni võtmerõnga tuvastamine ei leidnud toetatud taustateenust. Kui käivitad rakenduse %(brand)s käsurealt õigete argumentidega, siis võib taustateenuse käsitsi seadistamine õnnestuda ning seda tegevust peaksid vaid üks kord tegema. Lisateave: %(link)s.",
|
||||||
|
"unsupported_keyring_title": "Süsteem pole toetatud",
|
||||||
|
"unsupported_keyring_use_basic_text": "Kasuta nõrgemat krüptimist",
|
||||||
|
"unsupported_keyring_use_plaintext": "Ära üldse kasuta krüptimist"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Näita tavasuuruses",
|
"actual_size": "Näita tavasuuruses",
|
||||||
"toggle_developer_tools": "Arendaja töövahendid sisse/välja",
|
"toggle_developer_tools": "Arendaja töövahendid sisse/välja",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Tietoa",
|
"about": "Tietoa",
|
||||||
"brand_help": "%(brand)s-tuki",
|
"brand_help": "%(brand)s-tuki",
|
||||||
"help": "Ohje",
|
"help": "Ohje",
|
||||||
"preferences": "Valinnat"
|
"no": "Ei",
|
||||||
|
"preferences": "Valinnat",
|
||||||
|
"yes": "Kyllä"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Haluatko varmasti poistua?",
|
"confirm_quit": "Haluatko varmasti poistua?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,12 @@
|
|||||||
"save_image_as_error_description": "Kuvan tallennus epäonnistui",
|
"save_image_as_error_description": "Kuvan tallennus epäonnistui",
|
||||||
"save_image_as_error_title": "Kuvan tallennus epäonnistui"
|
"save_image_as_error_title": "Kuvan tallennus epäonnistui"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed_title": "Tietokannan lataaminen epäonnistui",
|
||||||
|
"unsupported_keyring_title": "Järjestelmä ei ole tuettu"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Alkuperäinen koko",
|
"actual_size": "Alkuperäinen koko",
|
||||||
"toggle_developer_tools": "Näytä tai piilota kehittäjätyökalut",
|
"toggle_developer_tools": "Näytä tai piilota kehittäjätyökalut",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "À propos",
|
"about": "À propos",
|
||||||
"brand_help": "Aide de %(brand)s",
|
"brand_help": "Aide de %(brand)s",
|
||||||
"help": "Aide",
|
"help": "Aide",
|
||||||
"preferences": "Préférences"
|
"no": "Non",
|
||||||
|
"preferences": "Préférences",
|
||||||
|
"yes": "Oui"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Êtes-vous sûr de vouloir quitter ?",
|
"confirm_quit": "Êtes-vous sûr de vouloir quitter ?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,16 @@
|
|||||||
"save_image_as_error_description": "L’image n’a pas pu être sauvegardée",
|
"save_image_as_error_description": "L’image n’a pas pu être sauvegardée",
|
||||||
"save_image_as_error_title": "Échec de la sauvegarde de l’image"
|
"save_image_as_error_title": "Échec de la sauvegarde de l’image"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Effacer les données et recharger ?",
|
||||||
|
"backend_changed_detail": "Impossible d'accéder aux secrets depuis le trousseau de clés du système, il semble avoir changé.",
|
||||||
|
"backend_changed_title": "Impossible de charger la base de données",
|
||||||
|
"unsupported_keyring": "Votre système possède un trousseau de clés non pris en charge, la base de données ne peut pas être ouverte.",
|
||||||
|
"unsupported_keyring_detail": "La détection du porte-clés par Electron n'a pas permis de trouver de backend compatible. Vous pouvez essayer de configurer manuellement le backend en utilisant %(brand)s avec un argument de ligne de commande. Cette opération doit être effectuer une seule fois. Voir%(link)s.",
|
||||||
|
"unsupported_keyring_title": "Système non pris en charge"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Taille réelle",
|
"actual_size": "Taille réelle",
|
||||||
"toggle_developer_tools": "Basculer les outils de développement",
|
"toggle_developer_tools": "Basculer les outils de développement",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Névjegy",
|
"about": "Névjegy",
|
||||||
"brand_help": "%(brand)s Súgó",
|
"brand_help": "%(brand)s Súgó",
|
||||||
"help": "Súgó",
|
"help": "Súgó",
|
||||||
"preferences": "Beállítások"
|
"no": "Nem",
|
||||||
|
"preferences": "Beállítások",
|
||||||
|
"yes": "Igen"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Biztos, hogy kilép?",
|
"confirm_quit": "Biztos, hogy kilép?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,16 @@
|
|||||||
"save_image_as_error_description": "A kép mentése sikertelen",
|
"save_image_as_error_description": "A kép mentése sikertelen",
|
||||||
"save_image_as_error_title": "Kép mentése sikertelen"
|
"save_image_as_error_title": "Kép mentése sikertelen"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Adatok törlése és újratöltés?",
|
||||||
|
"backend_changed_detail": "Nem sikerült hozzáférni a rendszerkulcstartó titkos kódjához, úgy tűnik, megváltozott.",
|
||||||
|
"backend_changed_title": "Nem sikerült betölteni az adatbázist",
|
||||||
|
"unsupported_keyring": "A rendszer nem támogatott kulcstartóval rendelkezik, ami azt jelenti, hogy az adatbázis nem nyitható meg.",
|
||||||
|
"unsupported_keyring_detail": "Az Electron kulcstartóészlelése nem talált támogatott háttérrendszert. Megpróbálhatja kézileg beállítani a háttérrendszert az %(brand)s egyszeri, parancssori argumentummal való indításával. Lásd: %(link)s.",
|
||||||
|
"unsupported_keyring_title": "A rendszer nem támogatott"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Jelenlegi méret",
|
"actual_size": "Jelenlegi méret",
|
||||||
"toggle_developer_tools": "Fejlesztői eszközök",
|
"toggle_developer_tools": "Fejlesztői eszközök",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Tentang",
|
"about": "Tentang",
|
||||||
"brand_help": "Bantuan %(brand)s",
|
"brand_help": "Bantuan %(brand)s",
|
||||||
"help": "Bantuan",
|
"help": "Bantuan",
|
||||||
"preferences": "Preferensi"
|
"no": "Tidak",
|
||||||
|
"preferences": "Preferensi",
|
||||||
|
"yes": "Ya"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Apakah Anda yakin ingin keluar?",
|
"confirm_quit": "Apakah Anda yakin ingin keluar?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,21 @@
|
|||||||
"save_image_as_error_description": "Gambar gagal disimpan",
|
"save_image_as_error_description": "Gambar gagal disimpan",
|
||||||
"save_image_as_error_title": "Gagal menyimpan gambar"
|
"save_image_as_error_title": "Gagal menyimpan gambar"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Hapus data dan muat ulang?",
|
||||||
|
"backend_changed_detail": "Tidak dapat mengakses rahasia dari keyring sistem, tampaknya telah berubah.",
|
||||||
|
"backend_changed_title": "Gagal memuat basis data",
|
||||||
|
"backend_no_encryption": "Sistem Anda memiliki keyring yang didukung tetapi enkripsi tidak tersedia.",
|
||||||
|
"backend_no_encryption_detail": "Electron telah mendeteksi bahwa enkripsi tidak tersedia pada keyring %(backend)s Anda. Harap pastikan bahwa Anda telah memasang keyring. Jika Anda telah memasang keyring, silakan mulai ulang dan coba lagi. Secara opsional, Anda dapat mengizinkan %(brand)s untuk menggunakan bentuk enkripsi yang lebih lemah.",
|
||||||
|
"backend_no_encryption_title": "Tidak ada dukungan enkripsi",
|
||||||
|
"unsupported_keyring": "Sistem Anda memiliki keyring yang tidak didukung yang berarti basis data tidak dapat dibuka.",
|
||||||
|
"unsupported_keyring_detail": "Deteksi keyring Electron tidak menemukan backend yang didukung. Anda dapat mencoba mengonfigurasi backend secara manual dengan memulai %(brand)s dengan argumen baris perintah, operasi satu kali. Lihat %(link)s.",
|
||||||
|
"unsupported_keyring_title": "Sistem tidak didukung",
|
||||||
|
"unsupported_keyring_use_basic_text": "Gunakan enkripsi yang lebih lemah",
|
||||||
|
"unsupported_keyring_use_plaintext": "Jangan gunakan enkripsi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Ukuran Sebenarnya",
|
"actual_size": "Ukuran Sebenarnya",
|
||||||
"toggle_developer_tools": "Beralih Alat Pengembang",
|
"toggle_developer_tools": "Beralih Alat Pengembang",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Om",
|
"about": "Om",
|
||||||
"brand_help": "%(brand)s Hjelp",
|
"brand_help": "%(brand)s Hjelp",
|
||||||
"help": "Hjelp",
|
"help": "Hjelp",
|
||||||
"preferences": "Brukervalg"
|
"no": "Nei",
|
||||||
|
"preferences": "Brukervalg",
|
||||||
|
"yes": "Ja"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Er du sikker på at du vil slutte?",
|
"confirm_quit": "Er du sikker på at du vil slutte?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,21 @@
|
|||||||
"save_image_as_error_description": "Bildet kunne ikke lagres",
|
"save_image_as_error_description": "Bildet kunne ikke lagres",
|
||||||
"save_image_as_error_title": "Kunne ikke lagre bildet"
|
"save_image_as_error_title": "Kunne ikke lagre bildet"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Tøm data og last inn på nytt?",
|
||||||
|
"backend_changed_detail": "Kan ikke få tilgang til hemmeligheten fra systemnøkkelringen, den ser ut til å ha blitt endret.",
|
||||||
|
"backend_changed_title": "Kunne ikke laste inn databasen",
|
||||||
|
"backend_no_encryption": "Systemet ditt har en støttet nøkkelring, men kryptering er ikke tilgjengelig.",
|
||||||
|
"backend_no_encryption_detail": "Electron har oppdaget at kryptering ikke er tilgjengelig på nøkkelringen %(backend)s din. Forsikre deg om at du har nøkkelringen installert. Hvis du har nøkkelringen installert, vennligst start på nytt og prøv igjen. Eventuelt kan du tillate %(brand)s å bruke en svakere form for kryptering.",
|
||||||
|
"backend_no_encryption_title": "Ingen støtte for kryptering",
|
||||||
|
"unsupported_keyring": "Systemet ditt har en nøkkelring som ikke støttes, noe som betyr at databasen ikke kan åpnes.",
|
||||||
|
"unsupported_keyring_detail": "Electrons nøkkelringdeteksjon fant ikke en støttet backend. Du kan prøve å konfigurere backend manuelt ved å starte %(brand)s med et kommandolinjeargument, en engangsoperasjon. Se%(link)s.",
|
||||||
|
"unsupported_keyring_title": "Systemet støttes ikke",
|
||||||
|
"unsupported_keyring_use_basic_text": "Bruk svakere kryptering",
|
||||||
|
"unsupported_keyring_use_plaintext": "Ikke bruk kryptering"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Faktisk størrelse",
|
"actual_size": "Faktisk størrelse",
|
||||||
"toggle_developer_tools": "Veksle Utvikleralternativer",
|
"toggle_developer_tools": "Veksle Utvikleralternativer",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Informacje",
|
"about": "Informacje",
|
||||||
"brand_help": "Pomoc %(brand)s",
|
"brand_help": "Pomoc %(brand)s",
|
||||||
"help": "Pomoc",
|
"help": "Pomoc",
|
||||||
"preferences": "Preferencje"
|
"no": "Nie",
|
||||||
|
"preferences": "Preferencje",
|
||||||
|
"yes": "Tak"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Czy na pewno chcesz zamknąć?",
|
"confirm_quit": "Czy na pewno chcesz zamknąć?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,16 @@
|
|||||||
"save_image_as_error_description": "Obraz nie został zapisany",
|
"save_image_as_error_description": "Obraz nie został zapisany",
|
||||||
"save_image_as_error_title": "Nie udało się zapisać obrazu"
|
"save_image_as_error_title": "Nie udało się zapisać obrazu"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Wyczyścić dane i przeładować?",
|
||||||
|
"backend_changed_detail": "Nie można uzyskać dostępu do sekretnego magazynu, wygląda na to, że uległ zmianie.",
|
||||||
|
"backend_changed_title": "Nie udało się załadować bazy danych",
|
||||||
|
"unsupported_keyring": "System zawiera niewspierany keyring, nie można otworzyć bazy danych.",
|
||||||
|
"unsupported_keyring_detail": "Wykrywanie keyringu Electron nie znalazł wspieranego backendu. Możesz spróbować ręcznie ustawić backed, uruchamiając %(brand)s za pomocą wiesza poleceń. Zobacz %(link)s.",
|
||||||
|
"unsupported_keyring_title": "System niewspierany"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Rozmiar rzeczywisty",
|
"actual_size": "Rozmiar rzeczywisty",
|
||||||
"toggle_developer_tools": "Przełącz narzędzia deweloperskie",
|
"toggle_developer_tools": "Przełącz narzędzia deweloperskie",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Sobre",
|
"about": "Sobre",
|
||||||
"brand_help": "%(brand)s Ajuda",
|
"brand_help": "%(brand)s Ajuda",
|
||||||
"help": "Ajuda",
|
"help": "Ajuda",
|
||||||
"preferences": "Preferências"
|
"no": "Não",
|
||||||
|
"preferences": "Preferências",
|
||||||
|
"yes": "Sim"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Você tem certeza que você quer sair?",
|
"confirm_quit": "Você tem certeza que você quer sair?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,21 @@
|
|||||||
"save_image_as_error_description": "A imagem falhou para salvar",
|
"save_image_as_error_description": "A imagem falhou para salvar",
|
||||||
"save_image_as_error_title": "Falha para salvar imagem"
|
"save_image_as_error_title": "Falha para salvar imagem"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Limpar dados e recarregar?",
|
||||||
|
"backend_changed_detail": "Não foi possível acessar o segredo no cofre do sistema, parece que ele foi alterado.",
|
||||||
|
"backend_changed_title": "Falha ao carregar o banco de dados",
|
||||||
|
"backend_no_encryption": "Seu sistema tem um cofre compatível, mas a criptografia não está disponível.",
|
||||||
|
"backend_no_encryption_detail": "O Electron detetou que a encriptação não está disponível no seu cofre %(backend)s. Certifique-se de que tem o cofre instalado. Se tiver o cofre instalado, reinicie e tente novamente. Opcionalmente, você pode permitir que %(brand)s use uma forma mais fraca de criptografia.",
|
||||||
|
"backend_no_encryption_title": "Sem suporte para criptografia",
|
||||||
|
"unsupported_keyring": "Seu sistema possui um cofre não compatível, o que impede a abertura do banco de dados.",
|
||||||
|
"unsupported_keyring_detail": "A detecção de cofre do Electron não encontrou um backend compatível. Você pode tentar configurar manualmente o backend iniciando %(brand)s com um argumento de linha de comando, uma operação única. Consulte %(link)s.",
|
||||||
|
"unsupported_keyring_title": "Sistema não suportado",
|
||||||
|
"unsupported_keyring_use_basic_text": "Use criptografia mais fraca",
|
||||||
|
"unsupported_keyring_use_plaintext": "Não usar criptografia"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Tamanho de Verdade",
|
"actual_size": "Tamanho de Verdade",
|
||||||
"toggle_developer_tools": "Ativar/Desativar Ferramentas de Desenvolvimento",
|
"toggle_developer_tools": "Ativar/Desativar Ferramentas de Desenvolvimento",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Informácie",
|
"about": "Informácie",
|
||||||
"brand_help": "%(brand)s Pomoc",
|
"brand_help": "%(brand)s Pomoc",
|
||||||
"help": "Pomocník",
|
"help": "Pomocník",
|
||||||
"preferences": "Predvoľby"
|
"no": "Nie",
|
||||||
|
"preferences": "Predvoľby",
|
||||||
|
"yes": "Áno"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Naozaj chcete zavrieť aplikáciu?",
|
"confirm_quit": "Naozaj chcete zavrieť aplikáciu?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,21 @@
|
|||||||
"save_image_as_error_description": "Obrázok sa nepodarilo uložiť",
|
"save_image_as_error_description": "Obrázok sa nepodarilo uložiť",
|
||||||
"save_image_as_error_title": "Chyba pri ukladaní obrázka"
|
"save_image_as_error_title": "Chyba pri ukladaní obrázka"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Vymazať údaje a znova načítať?",
|
||||||
|
"backend_changed_detail": "Nepodarilo sa získať prístup k tajnému kľúču zo systémového zväzku kľúčov, zdá sa, že sa zmenil.",
|
||||||
|
"backend_changed_title": "Nepodarilo sa načítať databázu",
|
||||||
|
"backend_no_encryption": "Váš systém má podporovaný zväzok kľúčov, ale šifrovanie nie je k dispozícii.",
|
||||||
|
"backend_no_encryption_detail": "Electron zistil, že šifrovanie nie je k dispozícii na vašom zväzku kľúčov %(backend)s. Uistite sa, že máte nainštalovaný zväzok kľúčov. Ak máte zväzok kľúčov nainštalovaný, reštartujte počítač a skúste to znova. Voliteľne môžete povoliť aplikácii %(brand)s používať slabšiu formu šifrovania.",
|
||||||
|
"backend_no_encryption_title": "Žiadna podpora šifrovania",
|
||||||
|
"unsupported_keyring": "Váš systém má nepodporovaný zväzok kľúčov, čo znamená, že databázu nemožno otvoriť.",
|
||||||
|
"unsupported_keyring_detail": "Detekcia zväzku kľúčov aplikácie Electron nenašla podporovaný backend. Môžete sa pokúsiť manuálne nastaviť backend spustením aplikácie %(brand)s s argumentom príkazového riadka, je to jednorazová operácia. Pozrite si %(link)s .",
|
||||||
|
"unsupported_keyring_title": "Systém nie je podporovaný",
|
||||||
|
"unsupported_keyring_use_basic_text": "Použiť slabšie šifrovanie",
|
||||||
|
"unsupported_keyring_use_plaintext": "Nepoužiť žiadne šifrovanie"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Aktuálna veľkosť",
|
"actual_size": "Aktuálna veľkosť",
|
||||||
"toggle_developer_tools": "Nástroje pre vývojárov",
|
"toggle_developer_tools": "Nástroje pre vývojárov",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"about": "Про застосунок",
|
"about": "Про застосунок",
|
||||||
"brand_help": "Довідка %(brand)s",
|
"brand_help": "Довідка %(brand)s",
|
||||||
"help": "Довідка",
|
"help": "Довідка",
|
||||||
"preferences": "Параметри"
|
"no": "Ні",
|
||||||
|
"preferences": "Параметри",
|
||||||
|
"yes": "Так"
|
||||||
},
|
},
|
||||||
"confirm_quit": "Ви впевнені, що хочете вийти?",
|
"confirm_quit": "Ви впевнені, що хочете вийти?",
|
||||||
"edit_menu": {
|
"edit_menu": {
|
||||||
@@ -49,6 +51,16 @@
|
|||||||
"save_image_as_error_description": "Не вдалося зберегти зображення",
|
"save_image_as_error_description": "Не вдалося зберегти зображення",
|
||||||
"save_image_as_error_title": "Не вдалося зберегти зображення"
|
"save_image_as_error_title": "Не вдалося зберегти зображення"
|
||||||
},
|
},
|
||||||
|
"store": {
|
||||||
|
"error": {
|
||||||
|
"backend_changed": "Очистити дані та перезавантажити?",
|
||||||
|
"backend_changed_detail": "Не вдається отримати доступ до таємного ключа з системного набору ключів, видається, він змінився.",
|
||||||
|
"backend_changed_title": "Не вдалося завантажити базу даних",
|
||||||
|
"unsupported_keyring": "Ваша система має непідтримуваний набір ключів. Це означає, що базу даних неможливо відкрити.",
|
||||||
|
"unsupported_keyring_detail": "Electron не виявив підтримуваного бекенда для роботи зі сховищем паролів. Ви можете вручну налаштувати його, запустивши %(brand)s з відповідним аргументом у командному рядку. Цю дію потрібно виконати лише один раз. Докладніше – %(link)s.",
|
||||||
|
"unsupported_keyring_title": "Система не підтримується"
|
||||||
|
}
|
||||||
|
},
|
||||||
"view_menu": {
|
"view_menu": {
|
||||||
"actual_size": "Фактичний розмір",
|
"actual_size": "Фактичний розмір",
|
||||||
"toggle_developer_tools": "Перемкнути інструменти розробника",
|
"toggle_developer_tools": "Перемкнути інструменти розробника",
|
||||||
|
|||||||
46
src/ipc.ts
46
src/ipc.ts
@@ -6,14 +6,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, autoUpdater, desktopCapturer, ipcMain, powerSaveBlocker, TouchBar, nativeImage } from "electron";
|
import { app, autoUpdater, desktopCapturer, ipcMain, powerSaveBlocker, TouchBar, nativeImage } from "electron";
|
||||||
import { relaunchApp } from "@standardnotes/electron-clear-data";
|
|
||||||
import keytar from "keytar-forked";
|
|
||||||
|
|
||||||
import IpcMainEvent = Electron.IpcMainEvent;
|
import IpcMainEvent = Electron.IpcMainEvent;
|
||||||
import { recordSSOSession } from "./protocol.js";
|
|
||||||
import { randomArray } from "./utils.js";
|
import { randomArray } from "./utils.js";
|
||||||
import { Settings } from "./settings.js";
|
import { Settings } from "./settings.js";
|
||||||
import { getDisplayMediaCallback, setDisplayMediaCallback } from "./displayMediaCallback.js";
|
import { getDisplayMediaCallback, setDisplayMediaCallback } from "./displayMediaCallback.js";
|
||||||
|
import Store, { clearDataAndRelaunch } from "./store.js";
|
||||||
|
|
||||||
ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void {
|
ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void {
|
||||||
if (process.platform !== "win32") {
|
if (process.platform !== "win32") {
|
||||||
@@ -61,7 +59,8 @@ ipcMain.on("app_onAction", function (_ev: IpcMainEvent, payload) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
||||||
if (!global.mainWindow) return;
|
const store = Store.instance;
|
||||||
|
if (!global.mainWindow || !store) return;
|
||||||
|
|
||||||
const args = payload.args || [];
|
const args = payload.args || [];
|
||||||
let ret: any;
|
let ret: any;
|
||||||
@@ -96,9 +95,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
global.mainWindow.focus();
|
global.mainWindow.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "getConfig":
|
|
||||||
ret = global.vectorConfig;
|
|
||||||
break;
|
|
||||||
case "navigateBack":
|
case "navigateBack":
|
||||||
if (global.mainWindow.webContents.canGoBack()) {
|
if (global.mainWindow.webContents.canGoBack()) {
|
||||||
global.mainWindow.webContents.goBack();
|
global.mainWindow.webContents.goBack();
|
||||||
@@ -113,11 +110,11 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
if (typeof args[0] !== "boolean") return;
|
if (typeof args[0] !== "boolean") return;
|
||||||
|
|
||||||
global.mainWindow.webContents.session.setSpellCheckerEnabled(args[0]);
|
global.mainWindow.webContents.session.setSpellCheckerEnabled(args[0]);
|
||||||
global.store.set("spellCheckerEnabled", args[0]);
|
store.set("spellCheckerEnabled", args[0]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "getSpellCheckEnabled":
|
case "getSpellCheckEnabled":
|
||||||
ret = global.store.get("spellCheckerEnabled", true);
|
ret = store.get("spellCheckerEnabled");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "setSpellCheckLanguages":
|
case "setSpellCheckLanguages":
|
||||||
@@ -135,18 +132,9 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages;
|
ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "startSSOFlow":
|
|
||||||
recordSSOSession(args[0]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "getPickleKey":
|
case "getPickleKey":
|
||||||
try {
|
try {
|
||||||
ret = await keytar.getPassword("element.io", `${args[0]}|${args[1]}`);
|
ret = await store.getSecret(`${args[0]}|${args[1]}`);
|
||||||
// migrate from riot.im (remove once we think there will no longer be
|
|
||||||
// logins from the time of riot.im)
|
|
||||||
if (ret === null) {
|
|
||||||
ret = await keytar.getPassword("riot.im", `${args[0]}|${args[1]}`);
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// if an error is thrown (e.g. keytar can't connect to the keychain),
|
// if an error is thrown (e.g. keytar can't connect to the keychain),
|
||||||
// then return null, which means the default pickle key will be used
|
// then return null, which means the default pickle key will be used
|
||||||
@@ -157,9 +145,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
case "createPickleKey":
|
case "createPickleKey":
|
||||||
try {
|
try {
|
||||||
const pickleKey = await randomArray(32);
|
const pickleKey = await randomArray(32);
|
||||||
// We purposefully throw if keytar is not available so the caller can handle it
|
await store.setSecret(`${args[0]}|${args[1]}`, pickleKey);
|
||||||
// rather than sending them a pickle key we did not store on their behalf.
|
|
||||||
await keytar!.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey);
|
|
||||||
ret = pickleKey;
|
ret = pickleKey;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to create pickle key", e);
|
console.error("Failed to create pickle key", e);
|
||||||
@@ -169,11 +155,10 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
|
|
||||||
case "destroyPickleKey":
|
case "destroyPickleKey":
|
||||||
try {
|
try {
|
||||||
await keytar.deletePassword("element.io", `${args[0]}|${args[1]}`);
|
await store.deleteSecret(`${args[0]}|${args[1]}`);
|
||||||
// migrate from riot.im (remove once we think there will no longer be
|
} catch (e) {
|
||||||
// logins from the time of riot.im)
|
console.error("Failed to destroy pickle key", e);
|
||||||
await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`);
|
}
|
||||||
} catch {}
|
|
||||||
break;
|
break;
|
||||||
case "getDesktopCapturerSources":
|
case "getDesktopCapturerSources":
|
||||||
ret = (await desktopCapturer.getSources(args[0])).map((source) => ({
|
ret = (await desktopCapturer.getSources(args[0])).map((source) => ({
|
||||||
@@ -189,10 +174,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "clearStorage":
|
case "clearStorage":
|
||||||
global.store.clear();
|
await clearDataAndRelaunch();
|
||||||
global.mainWindow.webContents.session.flushStorageData();
|
|
||||||
await global.mainWindow.webContents.session.clearStorageData();
|
|
||||||
relaunchApp();
|
|
||||||
return; // the app is about to stop, we don't need to reply to the IPC
|
return; // the app is about to stop, we don't need to reply to the IPC
|
||||||
|
|
||||||
case "breadcrumbs": {
|
case "breadcrumbs": {
|
||||||
@@ -259,3 +241,5 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
|||||||
reply: ret,
|
reply: ret,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("getConfig", () => global.vectorConfig);
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import { type TranslationKey as TKey } from "matrix-web-i18n";
|
|||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import type Store from "electron-store";
|
|
||||||
import type EN from "./i18n/strings/en_EN.json";
|
import type EN from "./i18n/strings/en_EN.json";
|
||||||
import { loadJsonFile } from "./utils.js";
|
import { loadJsonFile } from "./utils.js";
|
||||||
|
import type Store from "./store.js";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -59,26 +59,24 @@ export function _t(text: TranslationKey, variables: Variables = {}): string {
|
|||||||
|
|
||||||
type Component = () => void;
|
type Component = () => void;
|
||||||
|
|
||||||
type TypedStore = Store<{ locale?: string | string[] }>;
|
|
||||||
|
|
||||||
export class AppLocalization {
|
export class AppLocalization {
|
||||||
private static readonly STORE_KEY = "locale";
|
private static readonly STORE_KEY = "locale";
|
||||||
|
|
||||||
private readonly store: TypedStore;
|
|
||||||
private readonly localizedComponents?: Set<Component>;
|
private readonly localizedComponents?: Set<Component>;
|
||||||
|
private readonly store: Store;
|
||||||
|
|
||||||
public constructor({ store, components = [] }: { store: TypedStore; components: Component[] }) {
|
public constructor({ components = [], store }: { components: Component[]; store: Store }) {
|
||||||
counterpart.registerTranslations(FALLBACK_LOCALE, this.fetchTranslationJson("en_EN"));
|
counterpart.registerTranslations(FALLBACK_LOCALE, this.fetchTranslationJson("en_EN"));
|
||||||
counterpart.setFallbackLocale(FALLBACK_LOCALE);
|
counterpart.setFallbackLocale(FALLBACK_LOCALE);
|
||||||
counterpart.setSeparator("|");
|
counterpart.setSeparator("|");
|
||||||
|
|
||||||
|
this.store = store;
|
||||||
if (Array.isArray(components)) {
|
if (Array.isArray(components)) {
|
||||||
this.localizedComponents = new Set(components);
|
this.localizedComponents = new Set(components);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.store = store;
|
if (store.has(AppLocalization.STORE_KEY)) {
|
||||||
if (this.store.has(AppLocalization.STORE_KEY)) {
|
const locales = store.get(AppLocalization.STORE_KEY);
|
||||||
const locales = this.store.get(AppLocalization.STORE_KEY);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
this.setAppLocale(locales!);
|
this.setAppLocale(locales!);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,4 +49,16 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
}
|
}
|
||||||
ipcRenderer.send(channel, ...args);
|
ipcRenderer.send(channel, ...args);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async initialise(): Promise<{
|
||||||
|
protocol: string;
|
||||||
|
sessionId: string;
|
||||||
|
config: IConfigOptions;
|
||||||
|
}> {
|
||||||
|
const [{ protocol, sessionId }, config] = await Promise.all([
|
||||||
|
ipcRenderer.invoke("getProtocol"),
|
||||||
|
ipcRenderer.invoke("getConfig"),
|
||||||
|
]);
|
||||||
|
return { protocol, sessionId, config };
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
199
src/protocol.ts
199
src/protocol.ts
@@ -6,119 +6,136 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from "electron";
|
import { app, ipcMain } from "electron";
|
||||||
import { URL } from "node:url";
|
import { URL } from "node:url";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
const LEGACY_PROTOCOL = "element";
|
const LEGACY_PROTOCOL = "element";
|
||||||
const PROTOCOL = "io.element.desktop";
|
|
||||||
const SEARCH_PARAM = "element-desktop-ssoid";
|
const SEARCH_PARAM = "element-desktop-ssoid";
|
||||||
const STORE_FILE_NAME = "sso-sessions.json";
|
const STORE_FILE_NAME = "sso-sessions.json";
|
||||||
|
|
||||||
// we getPath userData before electron-main changes it, so this is the default value
|
// we getPath userData before electron-main changes it, so this is the default value
|
||||||
const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME);
|
const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME);
|
||||||
|
|
||||||
function processUrl(url: string): void {
|
export default class ProtocolHandler {
|
||||||
if (!global.mainWindow) return;
|
private readonly store: Record<string, string> = {};
|
||||||
|
private readonly sessionId: string;
|
||||||
|
|
||||||
const parsed = new URL(url);
|
public constructor(private readonly protocol: string) {
|
||||||
// sanity check: we only register for the one protocol, so we shouldn't
|
// get all args except `hidden` as it'd mean the app would not get focused
|
||||||
// be getting anything else unless the user is forcing a URL to open
|
// XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking
|
||||||
// with the Element app.
|
// --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url
|
||||||
if (parsed.protocol !== `${PROTOCOL}:` && parsed.protocol !== `${LEGACY_PROTOCOL}:`) {
|
const args = process.argv.slice(1).filter((arg) => arg !== "--hidden" && arg !== "-hidden");
|
||||||
console.log("Ignoring unexpected protocol: ", parsed.protocol);
|
if (app.isPackaged) {
|
||||||
return;
|
app.setAsDefaultProtocolClient(this.protocol, process.execPath, args);
|
||||||
|
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, args);
|
||||||
|
} else if (process.platform === "win32") {
|
||||||
|
// on Mac/Linux this would just cause the electron binary to open
|
||||||
|
// special handler for running without being packaged, e.g `electron .` by passing our app path to electron
|
||||||
|
app.setAsDefaultProtocolClient(this.protocol, process.execPath, [app.getAppPath(), ...args]);
|
||||||
|
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, [app.getAppPath(), ...args]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
// Protocol handler for macos
|
||||||
|
app.on("open-url", (ev, url) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.processUrl(url);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Protocol handler for win32/Linux
|
||||||
|
app.on("second-instance", (ev, commandLine) => {
|
||||||
|
const url = commandLine[commandLine.length - 1];
|
||||||
|
if (!url.startsWith(`${this.protocol}:/`) && !url.startsWith(`${LEGACY_PROTOCOL}://`)) return;
|
||||||
|
this.processUrl(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store = this.readStore();
|
||||||
|
this.sessionId = randomUUID();
|
||||||
|
|
||||||
|
ipcMain.handle("getProtocol", this.onGetProtocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlToLoad = new URL("vector://vector/webapp/");
|
private readonly onGetProtocol = (): { protocol: string; sessionId: string } => {
|
||||||
// ignore anything other than the search (used for SSO login redirect)
|
return {
|
||||||
// and the hash (for general element deep links)
|
protocol: this.protocol,
|
||||||
// There's no reason to allow anything else, particularly other paths,
|
sessionId: this.sessionId,
|
||||||
// since this would allow things like the internal jitsi wrapper to
|
};
|
||||||
// be loaded, which would get the app stuck on that page and generally
|
};
|
||||||
// be a bit strange and confusing.
|
|
||||||
urlToLoad.search = parsed.search;
|
|
||||||
urlToLoad.hash = parsed.hash;
|
|
||||||
|
|
||||||
console.log("Opening URL: ", urlToLoad.href);
|
private processUrl(url: string): void {
|
||||||
void global.mainWindow.loadURL(urlToLoad.href);
|
if (!global.mainWindow) return;
|
||||||
}
|
|
||||||
|
|
||||||
function readStore(): Record<string, string> {
|
const parsed = new URL(url);
|
||||||
try {
|
// sanity check: we only register for the one protocol, so we shouldn't
|
||||||
const s = fs.readFileSync(storePath, { encoding: "utf8" });
|
// be getting anything else unless the user is forcing a URL to open
|
||||||
const o = JSON.parse(s);
|
// with the Element app.
|
||||||
return typeof o === "object" ? o : {};
|
if (parsed.protocol !== `${this.protocol}:` && parsed.protocol !== `${LEGACY_PROTOCOL}:`) {
|
||||||
} catch {
|
console.log("Ignoring unexpected protocol: ", parsed.protocol);
|
||||||
return {};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlToLoad = new URL("vector://vector/webapp/");
|
||||||
|
// ignore anything other than the search (used for SSO login redirect)
|
||||||
|
// and the hash (for general element deep links)
|
||||||
|
// There's no reason to allow anything else, particularly other paths,
|
||||||
|
// since this would allow things like the internal jitsi wrapper to
|
||||||
|
// be loaded, which would get the app stuck on that page and generally
|
||||||
|
// be a bit strange and confusing.
|
||||||
|
urlToLoad.search = parsed.search;
|
||||||
|
urlToLoad.hash = parsed.hash;
|
||||||
|
|
||||||
|
console.log("Opening URL: ", urlToLoad.href);
|
||||||
|
void global.mainWindow.loadURL(urlToLoad.href);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function writeStore(data: Record<string, string>): void {
|
private readStore(): Record<string, string> {
|
||||||
fs.writeFileSync(storePath, JSON.stringify(data));
|
try {
|
||||||
}
|
const s = fs.readFileSync(storePath, { encoding: "utf8" });
|
||||||
|
const o = JSON.parse(s);
|
||||||
export function recordSSOSession(sessionID: string): void {
|
return typeof o === "object" ? o : {};
|
||||||
const userDataPath = app.getPath("userData");
|
} catch {
|
||||||
const store = readStore();
|
return {};
|
||||||
for (const key in store) {
|
|
||||||
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
|
|
||||||
if (store[key] === userDataPath) {
|
|
||||||
delete store[key];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
store[sessionID] = userDataPath;
|
|
||||||
writeStore(store);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProfileFromDeeplink(args: string[]): string | undefined {
|
private writeStore(): void {
|
||||||
// check if we are passed a profile in the SSO callback url
|
fs.writeFileSync(storePath, JSON.stringify(this.store));
|
||||||
const deeplinkUrl = args.find((arg) => arg.startsWith(`${PROTOCOL}://`) || arg.startsWith(`${LEGACY_PROTOCOL}://`));
|
}
|
||||||
if (deeplinkUrl?.includes(SEARCH_PARAM)) {
|
|
||||||
const parsedUrl = new URL(deeplinkUrl);
|
public initialise(userDataPath: string): void {
|
||||||
if (parsedUrl.protocol === `${PROTOCOL}:` || parsedUrl.protocol === `${LEGACY_PROTOCOL}:`) {
|
for (const key in this.store) {
|
||||||
const store = readStore();
|
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
|
||||||
let ssoID = parsedUrl.searchParams.get(SEARCH_PARAM);
|
if (this.store[key] === userDataPath) {
|
||||||
if (!ssoID) {
|
delete this.store[key];
|
||||||
// In OIDC, we must shuttle the value in the `state` param rather than `element-desktop-ssoid`
|
break;
|
||||||
// We encode it as a suffix like `:element-desktop-ssoid:XXYYZZ`
|
}
|
||||||
ssoID = parsedUrl.searchParams.get("state")!.split(`:${SEARCH_PARAM}:`)[1];
|
}
|
||||||
|
this.store[this.sessionId] = userDataPath;
|
||||||
|
this.writeStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getProfileFromDeeplink(args: string[]): string | undefined {
|
||||||
|
// check if we are passed a profile in the SSO callback url
|
||||||
|
const deeplinkUrl = args.find(
|
||||||
|
(arg) => arg.startsWith(`${this.protocol}://`) || arg.startsWith(`${LEGACY_PROTOCOL}://`),
|
||||||
|
);
|
||||||
|
if (deeplinkUrl?.includes(SEARCH_PARAM)) {
|
||||||
|
const parsedUrl = new URL(deeplinkUrl);
|
||||||
|
if (parsedUrl.protocol === `${this.protocol}:` || parsedUrl.protocol === `${LEGACY_PROTOCOL}:`) {
|
||||||
|
const store = this.readStore();
|
||||||
|
let sessionId = parsedUrl.searchParams.get(SEARCH_PARAM);
|
||||||
|
if (!sessionId) {
|
||||||
|
// In OIDC, we must shuttle the value in the `state` param rather than `element-desktop-ssoid`
|
||||||
|
// We encode it as a suffix like `:element-desktop-ssoid:XXYYZZ`
|
||||||
|
sessionId = parsedUrl.searchParams.get("state")!.split(`:${SEARCH_PARAM}:`)[1];
|
||||||
|
}
|
||||||
|
console.log("Forwarding to profile: ", store[sessionId]);
|
||||||
|
return store[sessionId];
|
||||||
}
|
}
|
||||||
console.log("Forwarding to profile: ", store[ssoID]);
|
|
||||||
return store[ssoID];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function protocolInit(): void {
|
|
||||||
// get all args except `hidden` as it'd mean the app would not get focused
|
|
||||||
// XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking
|
|
||||||
// --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url
|
|
||||||
const args = process.argv.slice(1).filter((arg) => arg !== "--hidden" && arg !== "-hidden");
|
|
||||||
if (app.isPackaged) {
|
|
||||||
app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, args);
|
|
||||||
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, args);
|
|
||||||
} else if (process.platform === "win32") {
|
|
||||||
// on Mac/Linux this would just cause the electron binary to open
|
|
||||||
// special handler for running without being packaged, e.g `electron .` by passing our app path to electron
|
|
||||||
app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [app.getAppPath(), ...args]);
|
|
||||||
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, [app.getAppPath(), ...args]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
|
||||||
// Protocol handler for macos
|
|
||||||
app.on("open-url", function (ev, url) {
|
|
||||||
ev.preventDefault();
|
|
||||||
processUrl(url);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Protocol handler for win32/Linux
|
|
||||||
app.on("second-instance", (ev, commandLine) => {
|
|
||||||
const url = commandLine[commandLine.length - 1];
|
|
||||||
if (!url.startsWith(`${PROTOCOL}:/`) && !url.startsWith(`${LEGACY_PROTOCOL}://`)) return;
|
|
||||||
processUrl(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import { app, ipcMain } from "electron";
|
import { app, ipcMain } from "electron";
|
||||||
import { promises as afs } from "node:fs";
|
import { promises as afs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import keytar from "keytar-forked";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Seshat as SeshatType,
|
Seshat as SeshatType,
|
||||||
@@ -17,6 +16,7 @@ import type {
|
|||||||
} from "matrix-seshat"; // Hak dependency type
|
} from "matrix-seshat"; // Hak dependency type
|
||||||
import IpcMainEvent = Electron.IpcMainEvent;
|
import IpcMainEvent = Electron.IpcMainEvent;
|
||||||
import { randomArray } from "./utils.js";
|
import { randomArray } from "./utils.js";
|
||||||
|
import Store from "./store.js";
|
||||||
|
|
||||||
let seshatSupported = false;
|
let seshatSupported = false;
|
||||||
let Seshat: typeof SeshatType;
|
let Seshat: typeof SeshatType;
|
||||||
@@ -40,21 +40,24 @@ try {
|
|||||||
let eventIndex: SeshatType | null = null;
|
let eventIndex: SeshatType | null = null;
|
||||||
|
|
||||||
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
|
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
|
||||||
async function getOrCreatePassphrase(key: string): Promise<string> {
|
async function getOrCreatePassphrase(store: Store, key: string): Promise<string> {
|
||||||
if (keytar) {
|
try {
|
||||||
try {
|
const storedPassphrase = await store.getSecret(key);
|
||||||
const storedPassphrase = await keytar.getPassword("element.io", key);
|
if (storedPassphrase !== null) {
|
||||||
if (storedPassphrase !== null) {
|
return storedPassphrase;
|
||||||
return storedPassphrase;
|
|
||||||
} else {
|
|
||||||
const newPassphrase = await randomArray(32);
|
|
||||||
await keytar.setPassword("element.io", key, newPassphrase);
|
|
||||||
return newPassphrase;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Error getting the event index passphrase out of the secret store", e);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error getting the event index passphrase out of the secret store", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPassphrase = await randomArray(32);
|
||||||
|
await store.setSecret(key, newPassphrase);
|
||||||
|
return newPassphrase;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error creating new event index passphrase, using default", e);
|
||||||
|
}
|
||||||
|
|
||||||
return seshatDefaultPassphrase;
|
return seshatDefaultPassphrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +77,8 @@ const deleteContents = async (p: string): Promise<void> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ipcMain.on("seshat", async function (_ev: IpcMainEvent, payload): Promise<void> {
|
ipcMain.on("seshat", async function (_ev: IpcMainEvent, payload): Promise<void> {
|
||||||
if (!global.mainWindow) return;
|
const store = Store.instance;
|
||||||
|
if (!global.mainWindow || !store) return;
|
||||||
|
|
||||||
// We do this here to ensure we get the path after --profile has been resolved
|
// We do this here to ensure we get the path after --profile has been resolved
|
||||||
const eventStorePath = path.join(app.getPath("userData"), "EventStore");
|
const eventStorePath = path.join(app.getPath("userData"), "EventStore");
|
||||||
@@ -101,7 +105,7 @@ ipcMain.on("seshat", async function (_ev: IpcMainEvent, payload): Promise<void>
|
|||||||
const deviceId = args[1];
|
const deviceId = args[1];
|
||||||
const passphraseKey = `seshat|${userId}|${deviceId}`;
|
const passphraseKey = `seshat|${userId}|${deviceId}`;
|
||||||
|
|
||||||
const passphrase = await getOrCreatePassphrase(passphraseKey);
|
const passphrase = await getOrCreatePassphrase(store, passphraseKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await afs.mkdir(eventStorePath, { recursive: true });
|
await afs.mkdir(eventStorePath, { recursive: true });
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as tray from "./tray.js";
|
import * as tray from "./tray.js";
|
||||||
|
import Store from "./store.js";
|
||||||
|
|
||||||
interface Setting {
|
interface Setting {
|
||||||
read(): Promise<any>;
|
read(): Promise<any>;
|
||||||
@@ -27,10 +28,10 @@ export const Settings: Record<string, Setting> = {
|
|||||||
},
|
},
|
||||||
"Electron.warnBeforeExit": {
|
"Electron.warnBeforeExit": {
|
||||||
async read(): Promise<any> {
|
async read(): Promise<any> {
|
||||||
return global.store.get("warnBeforeExit", true);
|
return Store.instance?.get("warnBeforeExit");
|
||||||
},
|
},
|
||||||
async write(value: any): Promise<void> {
|
async write(value: any): Promise<void> {
|
||||||
global.store.set("warnBeforeExit", value);
|
Store.instance?.set("warnBeforeExit", value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Electron.alwaysShowMenuBar": {
|
"Electron.alwaysShowMenuBar": {
|
||||||
@@ -39,7 +40,7 @@ export const Settings: Record<string, Setting> = {
|
|||||||
return !global.mainWindow!.autoHideMenuBar;
|
return !global.mainWindow!.autoHideMenuBar;
|
||||||
},
|
},
|
||||||
async write(value: any): Promise<void> {
|
async write(value: any): Promise<void> {
|
||||||
global.store.set("autoHideMenuBar", !value);
|
Store.instance?.set("autoHideMenuBar", !value);
|
||||||
global.mainWindow!.autoHideMenuBar = !value;
|
global.mainWindow!.autoHideMenuBar = !value;
|
||||||
global.mainWindow!.setMenuBarVisibility(value);
|
global.mainWindow!.setMenuBarVisibility(value);
|
||||||
},
|
},
|
||||||
@@ -56,15 +57,15 @@ export const Settings: Record<string, Setting> = {
|
|||||||
} else {
|
} else {
|
||||||
tray.destroy();
|
tray.destroy();
|
||||||
}
|
}
|
||||||
global.store.set("minimizeToTray", value);
|
Store.instance?.set("minimizeToTray", value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Electron.enableHardwareAcceleration": {
|
"Electron.enableHardwareAcceleration": {
|
||||||
async read(): Promise<any> {
|
async read(): Promise<any> {
|
||||||
return !global.store.get("disableHardwareAcceleration", false);
|
return !Store.instance?.get("disableHardwareAcceleration");
|
||||||
},
|
},
|
||||||
async write(value: any): Promise<void> {
|
async write(value: any): Promise<void> {
|
||||||
global.store.set("disableHardwareAcceleration", !value);
|
Store.instance?.set("disableHardwareAcceleration", !value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
505
src/store.ts
Normal file
505
src/store.ts
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022-2025 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ElectronStore from "electron-store";
|
||||||
|
import keytar from "keytar-forked";
|
||||||
|
import { app, safeStorage, dialog, type SafeStorage } from "electron";
|
||||||
|
import { clearAllUserData, relaunchApp } from "@standardnotes/electron-clear-data";
|
||||||
|
|
||||||
|
import { _t } from "./language-helper.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy keytar service name for storing secrets.
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
const KEYTAR_SERVICE = "element.io";
|
||||||
|
/**
|
||||||
|
* Super legacy keytar service name for reading secrets.
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
const LEGACY_KEYTAR_SERVICE = "riot.im";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String union type representing all the safeStorage backends.
|
||||||
|
* + The "unknown" backend shouldn't exist in practice once the app is ready
|
||||||
|
* + The "plaintext" is the temporarily-unencrypted backend for migration, data is wholly unencrypted - uses PlaintextStorageWriter
|
||||||
|
* + The "basic_text" backend is the 'plaintext' backend on Linux, data is encrypted but not using the keychain
|
||||||
|
* + The "system" backend is the encrypted backend on Windows & macOS, data is encrypted using system keychain
|
||||||
|
* + All other backends are linux-specific and are encrypted using the keychain
|
||||||
|
*/
|
||||||
|
type SafeStorageBackend = ReturnType<SafeStorage["getSelectedStorageBackend"]> | "system" | "plaintext";
|
||||||
|
/**
|
||||||
|
* The "unknown" backend is not a valid backend, so we exclude it from the type.
|
||||||
|
*/
|
||||||
|
type SaneSafeStorageBackend = Exclude<SafeStorageBackend, "unknown">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of safeStorage backends to their command line arguments.
|
||||||
|
* kwallet6 cannot be specified via command line
|
||||||
|
* https://www.electronjs.org/docs/latest/api/safe-storage#safestoragegetselectedstoragebackend-linux
|
||||||
|
*/
|
||||||
|
const safeStorageBackendMap: Omit<
|
||||||
|
Record<SafeStorageBackend, string>,
|
||||||
|
"unknown" | "kwallet6" | "system" | "plaintext"
|
||||||
|
> = {
|
||||||
|
basic_text: "basic",
|
||||||
|
gnome_libsecret: "gnome-libsecret",
|
||||||
|
kwallet: "kwallet",
|
||||||
|
kwallet5: "kwallet5",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all data and relaunch the app.
|
||||||
|
*/
|
||||||
|
export async function clearDataAndRelaunch(): Promise<void> {
|
||||||
|
Store.instance?.clear();
|
||||||
|
clearAllUserData();
|
||||||
|
relaunchApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoreData {
|
||||||
|
warnBeforeExit: boolean;
|
||||||
|
minimizeToTray: boolean;
|
||||||
|
spellCheckerEnabled: boolean;
|
||||||
|
autoHideMenuBar: boolean;
|
||||||
|
locale?: string | string[];
|
||||||
|
disableHardwareAcceleration: boolean;
|
||||||
|
safeStorage?: Record<string, string>;
|
||||||
|
/** the safeStorage backend used for the safeStorage data as written */
|
||||||
|
safeStorageBackend?: SafeStorageBackend;
|
||||||
|
/** whether to explicitly override the safeStorage backend, used for migration */
|
||||||
|
safeStorageBackendOverride?: boolean;
|
||||||
|
/** whether to perform a migration of the safeStorage data */
|
||||||
|
safeStorageBackendMigrate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback storage writer for secrets, mainly used for automated tests and systems without any safeStorage support.
|
||||||
|
*/
|
||||||
|
class StorageWriter {
|
||||||
|
public constructor(protected readonly store: ElectronStore<StoreData>) {}
|
||||||
|
|
||||||
|
public getKey(key: string): `safeStorage.${string}` {
|
||||||
|
return `safeStorage.${key.replaceAll(".", "-")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(key: string, secret: string): void {
|
||||||
|
this.store.set(this.getKey(key), secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: string): string | null {
|
||||||
|
return this.store.get(this.getKey(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public delete(key: string): void {
|
||||||
|
this.store.delete(this.getKey(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage writer for secrets using safeStorage.
|
||||||
|
*/
|
||||||
|
class SafeStorageWriter extends StorageWriter {
|
||||||
|
public set(key: string, secret: string): void {
|
||||||
|
this.store.set(this.getKey(key), safeStorage.encryptString(secret).toString("base64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: string): string | null {
|
||||||
|
const ciphertext = this.store.get<string, string | undefined>(this.getKey(key));
|
||||||
|
if (ciphertext) {
|
||||||
|
try {
|
||||||
|
return safeStorage.decryptString(Buffer.from(ciphertext, "base64"));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to decrypt secret", e);
|
||||||
|
console.error("...ciphertext:", JSON.stringify(ciphertext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum Mode {
|
||||||
|
Encrypted = "encrypted", // default
|
||||||
|
AllowPlaintext = "allow-plaintext",
|
||||||
|
ForcePlaintext = "force-plaintext",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON-backed store for settings which need to be accessible by the main process.
|
||||||
|
* Secrets are stored within the `safeStorage` object, encrypted with safeStorage.
|
||||||
|
* Any secrets operations are blocked on Electron app ready emit, and keytar migration if still needed.
|
||||||
|
*/
|
||||||
|
class Store extends ElectronStore<StoreData> {
|
||||||
|
private static internalInstance?: Store;
|
||||||
|
|
||||||
|
public static get instance(): Store | undefined {
|
||||||
|
return Store.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the store, does not prepare safeStorage, which needs to be done after the app is ready.
|
||||||
|
* Must be executed in the first tick of the event loop so that it can call Electron APIs before ready state.
|
||||||
|
*/
|
||||||
|
public static initialize(mode: Mode | undefined): Store {
|
||||||
|
if (Store.internalInstance) {
|
||||||
|
throw new Error("Store already initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new Store(mode ?? Mode.Encrypted);
|
||||||
|
Store.internalInstance = store;
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.platform === "linux" &&
|
||||||
|
(store.get("safeStorageBackendOverride") || store.get("safeStorageBackendMigrate"))
|
||||||
|
) {
|
||||||
|
const backend = store.get("safeStorageBackend")!;
|
||||||
|
if (backend in safeStorageBackendMap) {
|
||||||
|
// If the safeStorage backend which was used to write the data is one we can specify via the commandLine
|
||||||
|
// then do so to ensure we use the same backend for reading the data.
|
||||||
|
app.commandLine.appendSwitch(
|
||||||
|
"password-store",
|
||||||
|
safeStorageBackendMap[backend as keyof typeof safeStorageBackendMap],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provides "raw" access to the underlying secrets storage,
|
||||||
|
// should be avoided in favour of the getSecret/setSecret/deleteSecret methods.
|
||||||
|
private secrets?: StorageWriter;
|
||||||
|
|
||||||
|
private constructor(private mode: Mode) {
|
||||||
|
super({
|
||||||
|
name: "electron-config",
|
||||||
|
clearInvalidConfig: false,
|
||||||
|
schema: {
|
||||||
|
warnBeforeExit: {
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
minimizeToTray: {
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
spellCheckerEnabled: {
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
autoHideMenuBar: {
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
||||||
|
},
|
||||||
|
disableHardwareAcceleration: {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
safeStorage: {
|
||||||
|
type: "object",
|
||||||
|
},
|
||||||
|
safeStorageBackend: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
safeStorageBackendOverride: {
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
safeStorageBackendMigrate: {
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private safeStorageReadyPromise?: Promise<unknown>;
|
||||||
|
public async safeStorageReady(): Promise<void> {
|
||||||
|
if (!this.safeStorageReadyPromise) {
|
||||||
|
this.safeStorageReadyPromise = this.prepareSafeStorage();
|
||||||
|
}
|
||||||
|
await this.safeStorageReadyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise the backend to a sane value (exclude `unknown`), respect forcePlaintext mode,
|
||||||
|
* and ensure that if an encrypted backend is picked that encryption is available, falling back to plaintext if not.
|
||||||
|
* @param forcePlaintext - whether to force plaintext mode
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private chooseBackend(forcePlaintext: boolean): SaneSafeStorageBackend {
|
||||||
|
if (forcePlaintext) {
|
||||||
|
return "plaintext";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
// The following enables plain text encryption if the backend used is basic_text.
|
||||||
|
// It has no significance for any other backend.
|
||||||
|
// We do this early so that in case we end up using the basic_text backend (either because that's the only one available
|
||||||
|
// or as a fallback when the configured backend lacks encryption support), encryption is already turned on.
|
||||||
|
safeStorage.setUsePlainTextEncryption(true);
|
||||||
|
|
||||||
|
// Linux safeStorage support is hellish, the support varies on the Desktop Environment used rather than the store itself.
|
||||||
|
// https://github.com/electron/electron/issues/39789 https://github.com/microsoft/vscode/issues/185212
|
||||||
|
const selectedBackend = safeStorage.getSelectedStorageBackend();
|
||||||
|
|
||||||
|
if (selectedBackend === "unknown" || !safeStorage.isEncryptionAvailable()) {
|
||||||
|
return "plaintext";
|
||||||
|
}
|
||||||
|
return selectedBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeStorage.isEncryptionAvailable() ? "system" : "plaintext";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the safeStorage backend for use.
|
||||||
|
* We don't eagerly import from keytar as that would bring in data for all Element profiles and not just the current one,
|
||||||
|
* so we import lazily in getSecret.
|
||||||
|
*/
|
||||||
|
private async prepareSafeStorage(): Promise<void> {
|
||||||
|
await app.whenReady();
|
||||||
|
|
||||||
|
// The backend the existing data is written with if any
|
||||||
|
let existingSafeStorageBackend = this.get("safeStorageBackend");
|
||||||
|
// The backend and encryption status of the currently loaded backend
|
||||||
|
const backend = this.chooseBackend(this.mode === Mode.ForcePlaintext);
|
||||||
|
|
||||||
|
// Handle migrations
|
||||||
|
if (existingSafeStorageBackend) {
|
||||||
|
if (existingSafeStorageBackend === "basic_text" && backend !== "plaintext" && backend !== "basic_text") {
|
||||||
|
return this.prepareMigrateBasicTextToPlaintext();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get("safeStorageBackendMigrate") && backend === "basic_text") {
|
||||||
|
return this.migrateBasicTextToPlaintext();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingSafeStorageBackend === "plaintext" && backend !== "plaintext") {
|
||||||
|
this.migratePlaintextToEncrypted();
|
||||||
|
// Ensure we update existingSafeStorageBackend so we don't fall into the "backend changed" clause below
|
||||||
|
existingSafeStorageBackend = this.get("safeStorageBackend");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingSafeStorageBackend) {
|
||||||
|
// First launch of the app or first launch since the update
|
||||||
|
if (this.mode === Mode.Encrypted && (backend === "plaintext" || backend === "basic_text")) {
|
||||||
|
// Ask the user for consent to use a degraded mode
|
||||||
|
await this.consultUserConsentDegradedMode(backend);
|
||||||
|
}
|
||||||
|
// Store the backend used for the safeStorage data so we can detect if it changes, and we know how the data is encoded
|
||||||
|
this.recordSafeStorageBackend(backend);
|
||||||
|
} else if (existingSafeStorageBackend !== backend) {
|
||||||
|
console.warn(`safeStorage backend changed from ${existingSafeStorageBackend} to ${backend}`);
|
||||||
|
|
||||||
|
if (existingSafeStorageBackend in safeStorageBackendMap) {
|
||||||
|
this.set("safeStorageBackendOverride", true);
|
||||||
|
return relaunchApp();
|
||||||
|
} else {
|
||||||
|
await this.consultUserBackendChangedUnableToMigrate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`Using storage mode '${this.mode}' with backend '${backend}'`);
|
||||||
|
if (backend !== "plaintext") {
|
||||||
|
this.secrets = new SafeStorageWriter(this);
|
||||||
|
} else {
|
||||||
|
this.secrets = new StorageWriter(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async consultUserBackendChangedUnableToMigrate(): Promise<void> {
|
||||||
|
const { response } = await dialog.showMessageBox({
|
||||||
|
title: _t("store|error|backend_changed_title"),
|
||||||
|
message: _t("store|error|backend_changed"),
|
||||||
|
detail: _t("store|error|backend_changed_detail"),
|
||||||
|
type: "question",
|
||||||
|
buttons: [_t("common|no"), _t("common|yes")],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 0,
|
||||||
|
});
|
||||||
|
if (response === 0) {
|
||||||
|
throw new Error("safeStorage backend changed and cannot migrate");
|
||||||
|
}
|
||||||
|
return clearDataAndRelaunch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async consultUserConsentDegradedMode(backend: "plaintext" | "basic_text"): Promise<void> {
|
||||||
|
if (backend === "plaintext") {
|
||||||
|
// Sometimes we may have a working backend that for some reason does not support encryption at the moment.
|
||||||
|
// This may be because electron reported an incorrect backend or because of some known issues with the keyring itself.
|
||||||
|
// Or the environment specified `--storage-mode=force-plaintext`.
|
||||||
|
// In any case, when this happens, we give the user an option to use a weaker form of encryption.
|
||||||
|
const { response } = await dialog.showMessageBox({
|
||||||
|
title: _t("store|error|backend_no_encryption_title"),
|
||||||
|
message: _t("store|error|backend_no_encryption"),
|
||||||
|
detail: _t("store|error|backend_no_encryption_detail", {
|
||||||
|
backend: safeStorage.getSelectedStorageBackend(),
|
||||||
|
brand: global.vectorConfig.brand || "Element",
|
||||||
|
}),
|
||||||
|
type: "error",
|
||||||
|
buttons: [_t("action|cancel"), _t("store|error|unsupported_keyring_use_plaintext")],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 0,
|
||||||
|
});
|
||||||
|
if (response === 0) {
|
||||||
|
throw new Error("isEncryptionAvailable=false and user rejected plaintext");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Electron did not identify a compatible encrypted backend, ask user for consent to degraded mode
|
||||||
|
const { response } = await dialog.showMessageBox({
|
||||||
|
title: _t("store|error|unsupported_keyring_title"),
|
||||||
|
message: _t("store|error|unsupported_keyring"),
|
||||||
|
detail: _t("store|error|unsupported_keyring_detail", {
|
||||||
|
brand: global.vectorConfig.brand || "Element",
|
||||||
|
link: "https://www.electronjs.org/docs/latest/api/safe-storage#safestoragegetselectedstoragebackend-linux",
|
||||||
|
}),
|
||||||
|
type: "error",
|
||||||
|
buttons: [_t("action|cancel"), _t("store|error|unsupported_keyring_use_basic_text")],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 0,
|
||||||
|
});
|
||||||
|
if (response === 0) {
|
||||||
|
throw new Error("safeStorage backend basic_text and user rejected it");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private recordSafeStorageBackend(backend: SafeStorageBackend): void {
|
||||||
|
this.set("safeStorageBackend", backend);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linux support for upgrading the backend from basic_text to one of the encrypted backends,
|
||||||
|
* this is quite a tricky process as the backend is not known until the app is ready & cannot be changed once it is.
|
||||||
|
* 1. We restart the app in safeStorageBackendMigrate mode
|
||||||
|
* 2. Now that we are in the mode which our data is written in we decrypt the data, write it back in plaintext
|
||||||
|
* & restart back in default backend mode,
|
||||||
|
* 3. Finally, we load the plaintext data & encrypt it.
|
||||||
|
*/
|
||||||
|
private prepareMigrateBasicTextToPlaintext(): void {
|
||||||
|
console.info(`Starting safeStorage migration to ${safeStorage.getSelectedStorageBackend()}`);
|
||||||
|
this.set("safeStorageBackendMigrate", true);
|
||||||
|
relaunchApp();
|
||||||
|
}
|
||||||
|
private migrateBasicTextToPlaintext(): void {
|
||||||
|
const secrets = new SafeStorageWriter(this);
|
||||||
|
console.info("Performing safeStorage migration");
|
||||||
|
const data = this.get("safeStorage");
|
||||||
|
if (data) {
|
||||||
|
for (const key in data) {
|
||||||
|
this.set(secrets.getKey(key), secrets.get(key));
|
||||||
|
}
|
||||||
|
this.recordSafeStorageBackend("plaintext");
|
||||||
|
}
|
||||||
|
this.delete("safeStorageBackendMigrate");
|
||||||
|
relaunchApp();
|
||||||
|
}
|
||||||
|
private migratePlaintextToEncrypted(): void {
|
||||||
|
const secrets = new SafeStorageWriter(this);
|
||||||
|
const selectedSafeStorageBackend = safeStorage.getSelectedStorageBackend();
|
||||||
|
console.info(`Finishing safeStorage migration to ${selectedSafeStorageBackend}`);
|
||||||
|
const data = this.get("safeStorage");
|
||||||
|
if (data) {
|
||||||
|
for (const key in data) {
|
||||||
|
secrets.set(key, data[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.recordSafeStorageBackend(selectedSafeStorageBackend);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stored secret for the key.
|
||||||
|
* Lazily migrates keys from keytar if they are not yet in the store.
|
||||||
|
*
|
||||||
|
* @param key The string key name.
|
||||||
|
*
|
||||||
|
* @returns A promise for the secret string.
|
||||||
|
*/
|
||||||
|
public async getSecret(key: string): Promise<string | null> {
|
||||||
|
await this.safeStorageReady();
|
||||||
|
let secret = this.secrets!.get(key);
|
||||||
|
if (secret) return secret;
|
||||||
|
|
||||||
|
try {
|
||||||
|
secret = await this.getSecretKeytar(key);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to read data from keytar with key='${key}'`, e);
|
||||||
|
}
|
||||||
|
if (secret) {
|
||||||
|
console.debug("Migrating secret from keytar", key);
|
||||||
|
this.secrets!.set(key, secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the secret for the key to the keychain.
|
||||||
|
* We write to both safeStorage & keytar to support downgrading the application.
|
||||||
|
*
|
||||||
|
* @param key The string key name.
|
||||||
|
* @param secret The string password.
|
||||||
|
*
|
||||||
|
* @returns A promise for the set password completion.
|
||||||
|
*/
|
||||||
|
public async setSecret(key: string, secret: string): Promise<void> {
|
||||||
|
await this.safeStorageReady();
|
||||||
|
this.secrets!.set(key, secret);
|
||||||
|
try {
|
||||||
|
await keytar.setPassword(KEYTAR_SERVICE, key, secret);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to write safeStorage backwards-compatibility key='${key}' data to keytar`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the stored password for the key.
|
||||||
|
* Removes from safeStorage, keytar & keytar legacy.
|
||||||
|
*
|
||||||
|
* @param key The string key name.
|
||||||
|
*/
|
||||||
|
public async deleteSecret(key: string): Promise<void> {
|
||||||
|
await this.safeStorageReady();
|
||||||
|
this.secrets!.delete(key);
|
||||||
|
try {
|
||||||
|
await this.deleteSecretKeytar(key);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to delete secret with key='${key}' from keytar`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated will be removed in the near future
|
||||||
|
*/
|
||||||
|
private async getSecretKeytar(key: string): Promise<string | null> {
|
||||||
|
return (
|
||||||
|
(await keytar.getPassword(KEYTAR_SERVICE, key)) ?? (await keytar.getPassword(LEGACY_KEYTAR_SERVICE, key))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated will be removed in the near future
|
||||||
|
*/
|
||||||
|
private async deleteSecretKeytar(key: string): Promise<void> {
|
||||||
|
await keytar.deletePassword(LEGACY_KEYTAR_SERVICE, key);
|
||||||
|
await keytar.deletePassword(KEYTAR_SERVICE, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Store;
|
||||||
@@ -23,7 +23,7 @@ export async function randomArray(size: number): Promise<string> {
|
|||||||
|
|
||||||
type JsonValue = null | string | number;
|
type JsonValue = null | string | number;
|
||||||
type JsonArray = Array<JsonValue | JsonObject | JsonArray>;
|
type JsonArray = Array<JsonValue | JsonObject | JsonArray>;
|
||||||
interface JsonObject {
|
export interface JsonObject {
|
||||||
[key: string]: JsonObject | JsonArray | JsonValue;
|
[key: string]: JsonObject | JsonArray | JsonValue;
|
||||||
}
|
}
|
||||||
export type Json = JsonArray | JsonObject;
|
export type Json = JsonArray | JsonObject;
|
||||||
|
|||||||
Reference in New Issue
Block a user