Compare commits

..

1 Commits

Author SHA1 Message Date
David Baker
6e28c141ab Put electronVersion back (#287)
seems letting electron builder take it from the dependencies confuses
the keytar build process
2021-12-13 16:01:53 +00:00
104 changed files with 2663 additions and 8069 deletions

View File

@@ -12,8 +12,6 @@ module.exports = {
// we also have some browser code (ie. the preload script)
browser: true,
},
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
"quotes": "off",
"indent": "off",
@@ -21,7 +19,7 @@ module.exports = {
"no-async-promise-executor": "off",
},
overrides: [{
files: ["{src,scripts,hak}/**/*.{ts,tsx}"],
files: ["src/**/*.{ts,tsx}"],
extends: [
"plugin:matrix-org/typescript",
],
@@ -30,9 +28,8 @@ module.exports = {
"prefer-promise-reject-errors": "off",
"quotes": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
},
}],
};

View File

@@ -1,13 +0,0 @@
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
## Checklist
* [ ] Ensure your code works with manual testing
* [ ] Linter and other CI checks pass
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/vector-im/element-desktop/blob/develop/CONTRIBUTING.md))
<!--
If you would like to specify text for the changelog entry other than your PR title, add the following:
Notes: Add super cool feature
-->

View File

@@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>matrix-org/renovate-config-element-web"
]
}

View File

@@ -1,30 +0,0 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
branches:
- develop
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: tibdex/backport@v2
with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@@ -1,283 +0,0 @@
name: Build and Test
on:
pull_request: { }
push:
branches: [ develop, master ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fetch:
name: Prepare
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install --pure-lockfile"
- name: Fetch Element Web (develop)
if: github.event.pull_request.base.ref == 'develop'
run: yarn run fetch --noverify develop -d element.io/nightly
- name: Fetch Element Web
if: github.event.pull_request.base.ref != 'develop'
run: yarn run fetch --noverify --cfgdir element.io/release
- uses: actions/upload-artifact@v3
with:
name: webapp
retention-days: 1
path: |
webapp.asar
package.json
windows:
needs: fetch
strategy:
matrix:
include:
- target: x86_64-pc-windows-msvc
arch: x64
- target: i686-pc-windows-msvc
arch: x86
build-args: --ia32
name: Windows (${{ matrix.arch }})
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: webapp
- name: Cache .hak
uses: actions/cache@v3
with:
key: ${{ runner.os }}-${{ hashFiles('./yarn.lock') }}
path: |
./.hak
- name: Set up build tools
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.arch }}
# ActiveTCL package on choco is from 2015,
# this one is newer but includes more than we need
- name: Choco install tclsh
shell: pwsh
run: |
choco install -y magicsplat-tcl-tk --no-progress
echo "${HOME}/AppData/Local/Apps/Tcl86/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Choco install NetWide Assembler
shell: pwsh
run: |
choco install -y nasm --no-progress
echo "C:/Program Files/NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
- uses: actions/setup-node@v3
with:
cache: "yarn"
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install --pure-lockfile"
- name: Build Natives
run: |
refreshenv
yarn build:native --target ${{ matrix.target }}
- name: Build App
run: "yarn build --publish never -w ${{ matrix.build-args }}"
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: win-${{ matrix.arch }}
path: dist
retention-days: 1
linux:
needs: fetch
strategy:
matrix:
include:
- sqlcipher: system
- sqlcipher: static
static: 1
name: 'Linux (sqlcipher: ${{ matrix.sqlcipher }})'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: webapp
- name: Cache .hak
uses: actions/cache@v3
with:
key: ${{ hashFiles('./yarn.lock') }}
path: |
./.hak
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install libsqlcipher-dev
if: matrix.sqlcipher == 'system'
run: sudo apt-get install -y libsqlcipher-dev
- uses: actions/setup-node@v3
with:
cache: "yarn"
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install --pure-lockfile"
- name: Build Natives
run: "yarn build:native"
env:
SQLCIPHER_STATIC: ${{ matrix.static }}
- name: Build App
run: "yarn build --publish never"
- name: Install .deb
run: "sudo apt install ./dist/*.deb"
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: linux-sqlcipher-${{ matrix.sqlcipher }}
path: dist
retention-days: 1
macos:
needs: fetch
name: macOS (universal)
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: webapp
- name: Cache .hak
uses: actions/cache@v3
with:
key: ${{ hashFiles('./yarn.lock') }}
path: |
./.hak
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: aarch64-apple-darwin
- uses: actions/setup-node@v3
with:
cache: "yarn"
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install --pure-lockfile"
- name: Build Natives
run: "yarn build:native:universal"
- name: Build App
run: "yarn build:universal --publish never"
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: macos
path: dist
retention-days: 1
test:
needs:
- macos
- linux
- windows
strategy:
matrix:
include:
# Disable macOS tests for now, they fail to run in CI, needs investigation.
# - name: macOS Universal
# os: macos
# artifact: macos
# executable: "./dist/mac-universal/Element.app/Contents/MacOS/Element"
# prepare_cmd: "chmod +x ./dist/mac-universal/Element.app/Contents/MacOS/Element"
- name: 'Linux (sqlcipher: system)'
os: ubuntu
artifact: linux-sqlcipher-system
executable: "element-desktop"
prepare_cmd: "sudo apt install ./dist/*.deb"
- name: 'Linux (sqlcipher: static)'
os: ubuntu
artifact: linux-sqlcipher-static
executable: "element-desktop"
prepare_cmd: "sudo apt install ./dist/*.deb"
- name: Windows (x86)
os: windows
artifact: win-x86
executable: "./dist/win-ia32-unpacked/Element.exe"
- name: Windows (x64)
os: windows
artifact: win-x64
executable: "./dist/win-unpacked/Element.exe"
name: Test ${{ matrix.name }}
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install --pure-lockfile"
- uses: actions/download-artifact@v3
with:
name: ${{ matrix.artifact }}
path: dist
- name: Prepare for tests
run: ${{ matrix.prepare_cmd }}
if: matrix.prepare_cmd
- name: Run tests
uses: GabrielBB/xvfb-action@v1
timeout-minutes: 5
with:
run: "yarn test"
env:
ELEMENT_DESKTOP_EXECUTABLE: ${{ matrix.executable }}
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.artifact }}
path: test_artifacts
retention-days: 1

View File

@@ -0,0 +1,12 @@
name: Preview Changelog
on:
pull_request_target:
types: [ opened, edited, labeled ]
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Preview Changelog
uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,12 +0,0 @@
name: Pull Request
on:
pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ]
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
jobs:
action:
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
with:
labels: "T-Defect,T-Enhancement,T-Task"
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@@ -1,43 +0,0 @@
name: Static Analysis
on:
pull_request: { }
push:
branches: [ develop, master ]
jobs:
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
cache: 'yarn'
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install --pure-lockfile"
- name: Typecheck
run: "yarn run lint:types"
i18n_lint:
name: "i18n Check"
uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
cache: 'yarn'
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install --pure-lockfile"
- name: Run Linter
run: "yarn run lint:js"

View File

@@ -1,8 +0,0 @@
name: Upgrade Dependencies
on:
workflow_dispatch: { }
jobs:
upgrade:
uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

2
.gitignore vendored
View File

@@ -13,5 +13,3 @@
/.npmrc
.vscode
.vscode/
/test_artifacts/
/coverage/

View File

File diff suppressed because it is too large Load Diff

100
README.md
View File

@@ -1,10 +1,3 @@
![Build](https://github.com/vector-im/element-desktop/actions/workflows/build.yaml/badge.svg)
![Static Analysis](https://github.com/vector-im/element-desktop/actions/workflows/static_analysis.yaml/badge.svg)
[![Weblate](https://translate.element.io/widgets/element-desktop/-/element-desktop/svg-badge.svg)](https://translate.element.io/engage/element-desktop/)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=element-desktop)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=element-desktop)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=bugs)](https://sonarcloud.io/summary/new_code?id=element-desktop)
Element Desktop
===============
@@ -56,17 +49,30 @@ ln -s ../element-web/webapp ./
[TODO: add support for fetching develop builds, arbitrary URLs and arbitrary paths]
Building
========
Now you have a copy of Element, you're ready to build packages. If you'd just like to
run Element locally, skip to the next section.
## Native Build
If you'd like to build the native modules (for searching in encrypted rooms and
secure storage), do this first. This will take 10 minutes or so, and will
require a number of native tools to be installed, depending on your OS (eg.
rust, tcl, make/nmake).
TODO: List native pre-requisites
You'll also to need to make sure you've built the native modules for the same
architecture as your package, so for anything more advanced than just building
the modules and app for the host architecture see 'Other Architectures'.
Optionally, [build the native modules](https://github.com/vector-im/element-desktop/blob/develop/docs/native-node-modules.md),
which include support for searching in encrypted rooms and secure storage. Skipping this step is fine, you just won't have those features.
If you don't need these features, you can skip this step.
To just build these for your native architecture:
```
yarn run build:native
```
Now you can build the package:
Then, run
```
yarn run build
```
@@ -76,9 +82,9 @@ This will do a couple of things:
* Run electron-builder to build a package. The package built will match the operating system
you're running the build process on.
## Docker
This build step will not build any native modules.
Alternatively, you can also build using docker, which will always produce the linux package:
You can also build using docker, which will always produce the linux package:
```
# Run this once to make the docker image
yarn run docker:setup
@@ -101,6 +107,70 @@ yarn add electron
yarn start
```
Other Architectures
===================
Building the native modules will build for the host architecture (and only the
host architecture) by default. On Windows, this will automatically determine
the architecture to build for based on the environment. Make sure that you have
all the [tools required to perform the native modules build](docs/windows-requirements.md)
On macOS, you can build universal native modules too:
```
yarn run build:native:universal
```
...or you can build for a specific architecture:
```
yarn run build:native --target x86_64-apple-darwin
```
or
```
yarn run build:native --target aarch64-apple-darwin
```
You'll then need to create a built bundle with the same architecture.
To bundle a universal build for macOS, run:
```
yarn run build:universal
```
If you're on Windows, you can choose to build specifically for 32 or 64 bit:
```
yarn run build:32
```
or
```
yarn run build:64
```
Note that the native module build system keeps the different architectures
separate, so you can keep native modules for several architectures at the same
time and switch which are active using a `yarn run hak copy` command, passing
the appropriate architectures. This will error if you haven't yet built those
architectures. eg:
```
yarn run build:native --target x86_64-apple-darwin
# We've now built & linked into place native modules for Intel
yarn run build:native --target aarch64-apple-darwin
# We've now built Apple Silicon modules too, and linked them into place as the active ones
yarn run hak copy --target x86_64-apple-darwin
# We've now switched back to our Intel modules
yarn run hak copy --target x86_64-apple-darwin --target aarch64-apple-darwin
# Now our native modules are universal x86_64+aarch64 binaries
```
The current set of native modules are stored in `.hak/hakModules`,
so you can use this to check what architecture is currently in place, eg:
```
$ lipo -info .hak/hakModules/keytar/build/Release/keytar.node
Architectures in the fat file: .hak/hakModules/keytar/build/Release/keytar.node are: x86_64 arm64
```
Config
======
If you'd like the packaged Element to have a configuration file, you can create a
@@ -129,7 +199,7 @@ User-specified config.json
==========================
+ `%APPDATA%\$NAME\config.json` on Windows
+ `$XDG_CONFIG_HOME/$NAME/config.json` or `~/.config/$NAME/config.json` on Linux
+ `$XDG_CONFIG_HOME\$NAME\config.json` or `~/.config/$NAME/config.json` on Linux
+ `~/Library/Application Support/$NAME/config.json` on macOS
In the paths above, `$NAME` is typically `Element`, unless you use `--profile

View File

@@ -1,6 +0,0 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

View File

@@ -1,4 +1,4 @@
FROM buildpack-deps:bionic-curl
FROM buildpack-deps:xenial-curl
ENV DEBIAN_FRONTEND noninteractive
@@ -9,12 +9,11 @@ RUN apt-get -qq update && apt-get -qq dist-upgrade && \
# git ssh for using as docker image on CircleCI
# python for node-gyp
# rpm is required for FPM to build rpm package
# tclsh is required for building SQLite as part of SQLCipher
# libsecret-1-dev and libgnome-keyring-dev are required even for prebuild keytar
apt-get -qq install --no-install-recommends qtbase5-dev bsdtar build-essential autoconf libssl-dev gcc-multilib g++-multilib lzip rpm python libcurl4 git git-lfs ssh unzip tcl \
apt-get -qq install --no-install-recommends qtbase5-dev bsdtar build-essential autoconf libssl-dev gcc-multilib g++-multilib lzip rpm python libcurl3 git git-lfs ssh unzip \
libsecret-1-dev libgnome-keyring-dev \
libopenjp2-tools \
# Used by seshat (when not SQLCIPHER_STATIC) \
# Used by Seshat
libsqlcipher-dev && \
# git-lfs
git lfs install && \

View File

@@ -10,26 +10,20 @@ modules from source to ensure we can trust the compiled output. In the future,
we may offer a pre-compiled path for those who want to use these features in a
custom build of Element without installing the various build tools required.
Do note that compiling a module for a particular operating system
(Linux/macOS/Windows) will need to be done on that operating system.
Cross-compiling from a host OS for a different target OS may be possible, but
we don't support this flow with Element dependencies at this time.
The process is automated by [vector-im/element-builder](https://github.com/vector-im/element-builder)
when releasing.
when releasing.
The following sections explain the manual steps you can use with a custom build of Element to enable
these features if you'd like to try them out.
It is possible to [build those native modules locally automatically](https://github.com/vector-im/element-desktop#building).
## Building
Install the pre-requisites for your system:
* [Windows pre-requisites](https://github.com/vector-im/element-desktop/blob/develop/docs/windows-requirements.md)
* Linux: TODO
* OS X: TODO
Then optionally, [add seshat and dependencies to support search in E2E rooms](#adding-seshat-for-search-in-e2e-encrypted-rooms).
Then, to build for an architecture selected automatically based on your system (recommended), run:
```
yarn run build:native
```
If you need to build for a specific architecture, see [here](#compiling-for-specific-architectures).
## Adding Seshat for search in E2E encrypted rooms
Seshat is a native Node module that adds support for local event indexing and
@@ -47,7 +41,7 @@ using yarn at the root of this project:
yarn add matrix-seshat
You will have to rebuild the native libraries against electron's version
You will have to rebuild the native libraries against electron's version of
of node rather than your system node, using the `electron-build-env` tool.
This is also needed to when pulling in changes to Seshat using `yarn link`.
@@ -65,85 +59,3 @@ After this is done the Electron version of Element can be run from the main fold
as usual using:
yarn start
### Statically linking libsqlcipher
On Windows & macOS we always statically link libsqlcipher for it is not generally available.
On Linux by default we will use a system package, on debian & ubuntu this is `libsqlcipher0`,
but this is problematic for some other packages.
By including `SQLCIPHER_STATIC=1` in the build environment, the build scripts will statically link sqlcipher,
note that this will want a `libcrypto1.1` shared library available in the system.
More info can be found at https://github.com/matrix-org/seshat/issues/102
and https://github.com/vector-im/element-web/issues/20926.
## Compiling for specific architectures
### macOS
On macOS, you can build universal native modules too:
```
yarn run build:native:universal
```
...or you can build for a specific architecture:
```
yarn run build:native --target x86_64-apple-darwin
```
or
```
yarn run build:native --target aarch64-apple-darwin
```
You'll then need to create a built bundle with the same architecture.
To bundle a universal build for macOS, run:
```
yarn run build:universal
```
### Windows
If you're on Windows, you can choose to build specifically for 32 or 64 bit:
```
yarn run build:32
```
or
```
yarn run build:64
```
### Cross compiling
Compiling a module for a particular operating system (Linux/macOS/Windows) needs
to be done on that operating system. Cross-compiling from a host OS for a different
target OS may be possible, but we don't support this flow with Element dependencies
at this time.
### Switching between architectures
The native module build system keeps the different architectures
separate, so you can keep native modules for several architectures at the same
time and switch which are active using a `yarn run hak copy` command, passing
the appropriate architectures. This will error if you haven't yet built those
architectures. eg:
```
yarn run build:native --target x86_64-apple-darwin
# We've now built & linked into place native modules for Intel
yarn run build:native --target aarch64-apple-darwin
# We've now built Apple Silicon modules too, and linked them into place as the active ones
yarn run hak copy --target x86_64-apple-darwin
# We've now switched back to our Intel modules
yarn run hak copy --target x86_64-apple-darwin --target aarch64-apple-darwin
# Now our native modules are universal x86_64+aarch64 binaries
```
The current set of native modules are stored in `.hak/hakModules`,
so you can use this to check what architecture is currently in place, eg:
```
$ lipo -info .hak/hakModules/keytar/build/Release/keytar.node
Architectures in the fat file: .hak/hakModules/keytar/build/Release/keytar.node are: x86_64 arm64
```

View File

@@ -4,12 +4,10 @@
If you want to build native modules, make sure that the following tools are installed on your system.
- [Git for Windows](https://git-scm.com/download/win)
- [Node 14](https://nodejs.org)
- [Python 3](https://www.python.org/downloads/) (if you type 'python' into command prompt it will offer to install it from the windows store)
- [Python 3](https://www.python.org/downloads/)
- [Strawberry Perl](https://strawberryperl.com/)
- [Rustup](https://rustup.rs/)
- [NASM](https://www.nasm.us/)
- [Rust](https://rustup.rs/)
- [Build Tools for Visual Studio 2019](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019) with the following configuration:
- On the Workloads tab:
- Desktop & Mobile -> C++ build tools
@@ -19,17 +17,10 @@ If you want to build native modules, make sure that the following tools are inst
- C++ CMake tools for Windows
Once installed make sure all those utilities are accessible in your `PATH`.
If you want to be able to build x86 targets from an x64 host install the right toolchain:
```cmd
rustup toolchain install stable-i686-pc-windows-msvc
rustup target add i686-pc-windows-msvc
```
In order to load all the C++ utilities installed by Visual Studio you can run the following in a terminal window.
```
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" amd64
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64
```
You can replace `amd64` with `x86` depending on your CPU architecture.

View File

@@ -13,8 +13,12 @@
],
"hosting_signup_link": "https://element.io/matrix-services?utm_source=element-web&utm_medium=web",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"showLabsSettings": true,
"piwik": {
"url": "https://piwik.riot.im/",
"siteId": 1,
"policyUrl": "https://element.io/cookie-policy"
},
"roomDirectory": {
"servers": [
"matrix.org",
@@ -42,15 +46,9 @@
},
"posthog": {
"projectApiKey": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
"apiHost": "https://posthog.element.io"
"apiHost": "https://posthog.hss.element.io"
},
"privacy_policy_url": "https://element.io/cookie-policy",
"features": {
"feature_spotlight": true,
"feature_video_rooms": true
},
"element_call": {
"url": "https://element-call.netlify.app"
},
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
"feature_spaces_metaspaces": true
}
}

View File

@@ -3,10 +3,10 @@ License: Apache-2.0
Vendor: support@element.io
Architecture: amd64
Maintainer: support@element.io
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libsecret-1-0
Recommends: libappindicator3-1, libsqlcipher0
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libsecret-1-0, libsqlcipher0
Recommends: libappindicator3-1
Section: net
Priority: extra
Homepage: https://element.io/
Description:
Description:
riot.im A feature-rich client for Matrix.org (nightly unstable build).

View File

@@ -13,7 +13,6 @@
],
"hosting_signup_link": "https://element.io/matrix-services?utm_source=element-web&utm_medium=web",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"roomDirectory": {
"servers": [
"matrix.org",
@@ -22,6 +21,11 @@
]
},
"showLabsSettings": false,
"piwik": {
"url": "https://piwik.riot.im/",
"siteId": 1,
"policyUrl": "https://element.io/cookie-policy"
},
"enable_presence_by_hs_url": {
"https://matrix.org": false,
"https://matrix-client.matrix.org": false
@@ -35,11 +39,5 @@
"url": "https://element.io/cookie-policy",
"text": "Cookie Policy"
}
],
"posthog": {
"projectApiKey": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
"apiHost": "https://posthog.element.io"
},
"privacy_policy_url": "https://element.io/cookie-policy",
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
]
}

View File

@@ -3,12 +3,12 @@ License: Apache-2.0
Vendor: support@element.io
Architecture: amd64
Maintainer: support@element.io
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libsecret-1-0
Recommends: libappindicator3-1, libsqlcipher0
Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libuuid1, libsecret-1-0, libsqlcipher0
Recommends: libappindicator3-1
Replaces: riot-desktop (<< 1.7.0), riot-web (<< 1.7.0)
Breaks: riot-desktop (<< 1.7.0), riot-web (<< 1.7.0)
Section: net
Priority: extra
Homepage: https://element.io/
Description:
Description:
A feature-rich client for Matrix.org

View File

@@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import childProcess from 'child_process';
const path = require('path');
const childProcess = require('child_process');
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
module.exports = async function(hakEnv, moduleInfo) {
await buildKeytar(hakEnv, moduleInfo);
};
export default async function buildKeytar(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function buildKeytar(hakEnv, moduleInfo) {
const env = hakEnv.makeGypEnv();
console.log("Running yarn with env", env);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
path.join(moduleInfo.nodeModuleBinDir, 'node-gyp' + (hakEnv.isWin() ? '.cmd' : '')),
['rebuild'],

View File

@@ -14,16 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import childProcess from 'child_process';
const childProcess = require('child_process');
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
module.exports = async function(hakEnv, moduleInfo) {
const tools = [['python', '--version']]; // node-gyp uses python for reasons beyond comprehension
for (const tool of tools) {
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(tool[0], tool.slice(1), {
stdio: ['ignore'],
});
@@ -36,4 +33,4 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
});
});
}
}
};

View File

@@ -1,7 +1,7 @@
{
"scripts": {
"check": "check.ts",
"build": "build.ts"
"check": "check.js",
"build": "build.js"
},
"copy": "build/Release/keytar.node",
"dependencies": {

View File

@@ -14,40 +14,38 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import childProcess from 'child_process';
import mkdirp from 'mkdirp';
import fsExtra from 'fs-extra';
const path = require('path');
const childProcess = require('child_process');
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
const mkdirp = require('mkdirp');
const fsExtra = require('fs-extra');
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
module.exports = async function(hakEnv, moduleInfo) {
if (hakEnv.isWin()) {
await buildOpenSslWin(hakEnv, moduleInfo);
await buildSqlCipherWin(hakEnv, moduleInfo);
} else if (hakEnv.wantsStaticSqlCipherUnix()) {
} else if (hakEnv.isMac()) {
await buildSqlCipherUnix(hakEnv, moduleInfo);
}
await buildMatrixSeshat(hakEnv, moduleInfo);
}
};
async function buildOpenSslWin(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
async function buildOpenSslWin(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.openssl;
const openSslDir = path.join(moduleInfo.moduleTargetDotHakDir, `openssl-${version}`);
const openSslArch = hakEnv.getTargetArch() === 'x64' ? 'VC-WIN64A' : 'VC-WIN32';
console.log("Building openssl in " + openSslDir);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'perl',
[
'Configure',
'--prefix=' + moduleInfo.depPrefix,
// sqlcipher only uses about a tiny part of openssl. We link statically
// so will only pull in the symbols we use, but we may as well turn off
// as much as possible to save on build time.
// sqlcipher only uses about a tiny part of openssl. We link statically
// so will only pull in the symbols we use, but we may as well turn off
// as much as possible to save on build time.
'no-afalgeng',
'no-capieng',
'no-cms',
@@ -105,7 +103,7 @@ async function buildOpenSslWin(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'nmake',
['build_libs'],
@@ -119,7 +117,7 @@ async function buildOpenSslWin(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'nmake',
['install_dev'],
@@ -134,14 +132,14 @@ async function buildOpenSslWin(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
});
}
async function buildSqlCipherWin(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
async function buildSqlCipherWin(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.sqlcipher;
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
const buildDir = path.join(sqlCipherDir, 'bld');
await mkdirp(buildDir);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'nmake',
['/f', path.join('..', 'Makefile.msc'), 'libsqlite3.lib', 'TOP=..'],
@@ -171,7 +169,7 @@ async function buildSqlCipherWin(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
);
}
async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
async function buildSqlCipherUnix(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.sqlcipher;
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
@@ -179,21 +177,12 @@ async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
'--prefix=' + moduleInfo.depPrefix + '',
'--enable-tempstore=yes',
'--enable-shared=no',
'--enable-tcl=no',
];
if (hakEnv.isMac()) {
args.push('--with-crypto-lib=commoncrypto');
}
if (hakEnv.wantsStaticSqlCipherUnix()) {
args.push('--enable-tcl=no');
if (hakEnv.isLinux()) {
args.push('--with-pic=yes');
}
}
if (!hakEnv.isHost()) {
// In the nonsense world of `configure`, it is assumed you are building
// a compiler like `gcc`, so the `host` option actually means the target
@@ -214,7 +203,7 @@ async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
args.push(`CFLAGS=${cflags.join(' ')}`);
}
const ldflags: string[] = [];
const ldflags = [];
if (hakEnv.isMac()) {
ldflags.push('-framework Security');
@@ -225,7 +214,7 @@ async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
args.push(`LDFLAGS=${ldflags.join(' ')}`);
}
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
path.join(sqlCipherDir, 'configure'),
args,
@@ -239,7 +228,7 @@ async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'make',
[],
@@ -253,7 +242,7 @@ async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
'make',
['install'],
@@ -268,13 +257,13 @@ async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
});
}
async function buildMatrixSeshat(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
async function buildMatrixSeshat(hakEnv, moduleInfo) {
// seshat now uses n-api so we shouldn't need to specify a node version to
// build against, but it does seems to still need something in here, so leaving
// it for now: we should confirm how much of this it still actually needs.
const env = hakEnv.makeGypEnv();
if (!hakEnv.isLinux() || hakEnv.wantsStaticSqlCipherUnix()) {
if (!hakEnv.isLinux()) {
Object.assign(env, {
SQLCIPHER_STATIC: 1,
SQLCIPHER_LIB_DIR: path.join(moduleInfo.depPrefix, 'lib'),
@@ -282,25 +271,6 @@ async function buildMatrixSeshat(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
});
}
if (hakEnv.isLinux() && hakEnv.wantsStaticSqlCipherUnix()) {
// Ensure Element uses the statically-linked seshat build, and prevent other applications
// from attempting to use this one. Detailed explanation:
//
// RUSTFLAGS
// An environment variable containing a list of arguments to pass to rustc.
// -Clink-arg=VALUE
// A rustc argument to pass a single argument to the linker.
// -Wl,
// gcc syntax to pass an argument (from gcc) to the linker (ld).
// -Bsymbolic:
// Prefer local/statically linked symbols over those in the environment.
// Prevent overriding native libraries by LD_PRELOAD etc.
// --exclude-libs ALL
// Prevent symbols from being exported by any archive libraries.
// Reduces output filesize and prevents being dynamically linked against.
env.RUSTFLAGS = '-Clink-arg=-Wl,-Bsymbolic -Clink-arg=-Wl,--exclude-libs,ALL';
}
if (hakEnv.isWin()) {
env.RUSTFLAGS = '-Ctarget-feature=+crt-static -Clink-args=libcrypto.lib';
// Note that in general, you can specify targets in Rust without having to have
@@ -316,7 +286,7 @@ async function buildMatrixSeshat(hakEnv: HakEnv, moduleInfo: DependencyInfo) {
}
console.log("Running neon with env", env);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
path.join(moduleInfo.nodeModuleBinDir, 'neon' + (hakEnv.isWin() ? '.cmd' : '')),
['build', '--release'],

View File

@@ -14,16 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import childProcess from 'child_process';
import fsProm from 'fs/promises';
const childProcess = require('child_process');
const fsProm = require('fs').promises;
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (hakEnv.wantsStaticSqlCipher()) {
// of course tcl doesn't have a --version
await new Promise<void>((resolve, reject) => {
module.exports = async function(hakEnv, moduleInfo) {
// of course tcl doesn't have a --version
if (!hakEnv.isLinux()) {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn('tclsh', [], {
stdio: ['pipe', 'ignore', 'ignore'],
});
@@ -44,7 +41,6 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
];
if (hakEnv.isWin()) {
tools.push(['perl', '--version']); // for openssl configure
tools.push(['nasm', '-v']); // for openssl building
tools.push(['patch', '--version']); // to patch sqlcipher Makefile.msc
tools.push(['nmake', '/?']);
} else {
@@ -52,7 +48,7 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
}
for (const tool of tools) {
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(tool[0], tool.slice(1), {
stdio: ['ignore'],
});
@@ -83,4 +79,4 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
rustc.stdin.write('fn main() {}');
rustc.stdin.end();
});
}
};

View File

@@ -14,31 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import childProcess from 'child_process';
import fs from 'fs';
import fsProm from 'fs/promises';
import needle from 'needle';
import tar from 'tar';
const path = require('path');
const childProcess = require('child_process');
import HakEnv from '../../scripts/hak/hakEnv';
import { DependencyInfo } from '../../scripts/hak/dep';
const fs = require('fs');
const fsProm = require('fs').promises;
const needle = require('needle');
const tar = require('tar');
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (hakEnv.wantsStaticSqlCipher()) {
module.exports = async function(hakEnv, moduleInfo) {
if (!hakEnv.isLinux()) {
await getSqlCipher(hakEnv, moduleInfo);
}
if (hakEnv.isWin()) {
await getOpenSsl(hakEnv, moduleInfo);
}
}
};
async function getSqlCipher(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function getSqlCipher(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.sqlcipher;
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
let haveSqlcipher: boolean;
let haveSqlcipher;
try {
await fsProm.stat(sqlCipherDir);
haveSqlcipher = true;
@@ -49,7 +47,7 @@ async function getSqlCipher(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise
if (haveSqlcipher) return;
const sqlCipherTarball = path.join(moduleInfo.moduleDotHakDir, `sqlcipher-${version}.tar.gz`);
let haveSqlcipherTar: boolean;
let haveSqlcipherTar;
try {
await fsProm.stat(sqlCipherTarball);
haveSqlcipherTar = true;
@@ -76,8 +74,8 @@ async function getSqlCipher(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise
// set it to 2 (default to memory).
const patchFile = path.join(moduleInfo.moduleHakDir, `sqlcipher-${version}-win.patch`);
await new Promise<void>((resolve, reject) => {
const readStream = fs.createReadStream(patchFile);
await new Promise((resolve, reject) => {
const readStream = fs.createReadStream(patchFile);
const proc = childProcess.spawn(
'patch',
@@ -95,11 +93,11 @@ async function getSqlCipher(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise
}
}
async function getOpenSsl(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function getOpenSsl(hakEnv, moduleInfo) {
const version = moduleInfo.cfg.dependencies.openssl;
const openSslDir = path.join(moduleInfo.moduleTargetDotHakDir, `openssl-${version}`);
let haveOpenSsl: boolean;
let haveOpenSsl;
try {
await fsProm.stat(openSslDir);
haveOpenSsl = true;
@@ -110,7 +108,7 @@ async function getOpenSsl(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<v
if (haveOpenSsl) return;
const openSslTarball = path.join(moduleInfo.moduleDotHakDir, `openssl-${version}.tar.gz`);
let haveOpenSslTar: boolean;
let haveOpenSslTar;
try {
await fsProm.stat(openSslTarball);
haveOpenSslTar = true;

View File

@@ -1,8 +1,8 @@
{
"scripts": {
"check": "check.ts",
"fetchDeps": "fetchDeps.ts",
"build": "build.ts"
"check": "check.js",
"fetchDeps": "fetchDeps.js",
"build": "build.js"
},
"prune": "native",
"copy": "native/index.node",

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"target": "es2016",
"sourceMap": false,
"lib": [
"es2019",
]
},
"include": [
"./**/*.ts"
],
"ts-node": {
"transpileOnly": true
}
}

View File

@@ -2,7 +2,7 @@
"name": "element-desktop",
"productName": "Element",
"main": "lib/electron-main.js",
"version": "1.11.13",
"version": "1.9.7",
"description": "A feature-rich client for Matrix.org",
"author": "Element",
"repository": {
@@ -22,7 +22,7 @@
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src scripts hak",
"lint:js-fix": "eslint --fix src scripts hak",
"lint:types": "tsc --noEmit && tsc -p scripts/hak/tsconfig.json --noEmit && tsc -p hak/tsconfig.json --noEmit",
"lint:types": "tsc --noEmit",
"build:native": "yarn run hak",
"build:native:universal": "yarn run hak --target x86_64-apple-darwin fetchandbuild && yarn run hak --target aarch64-apple-darwin fetchandbuild && yarn run hak --target x86_64-apple-darwin --target aarch64-apple-darwin copyandlink",
"build:32": "yarn run build:ts && yarn run build:res && electron-builder --ia32",
@@ -37,74 +37,53 @@
"docker:install": "scripts/in-docker.sh yarn install",
"debrepo": "scripts/mkrepo.sh",
"clean": "rimraf webapp.asar dist packages deploys lib",
"hak": "ts-node scripts/hak/index.ts",
"test": "jest"
"hak": "node scripts/hak/index.js"
},
"dependencies": {
"auto-launch": "^5.0.5",
"counterpart": "^0.18.6",
"electron-store": "^8.0.2",
"electron-store": "^6.0.1",
"electron-window-state": "^5.0.3",
"minimist": "^1.2.6",
"minimist": "^1.2.3",
"png-to-ico": "^2.1.1",
"request": "^2.88.2"
},
"devDependencies": {
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/preset-typescript": "^7.18.6",
"@types/auto-launch": "^5.0.1",
"@types/counterpart": "^0.18.1",
"@types/detect-libc": "^1.0.0",
"@types/jest": "^28",
"@types/minimist": "^1.2.1",
"@types/mkdirp": "^1.0.2",
"@types/pacote": "^11.1.1",
"@types/rimraf": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"allchange": "^1.0.6",
"app-builder-lib": "^22.14.10",
"asar": "^2.0.1",
"babel-jest": "^28.1.3",
"chokidar": "^3.5.2",
"detect-libc": "^1.0.3",
"electron": "^20",
"electron": "13.5",
"electron-builder": "22.11.4",
"electron-builder-squirrel-windows": "22.11.4",
"electron-devtools-installer": "^3.1.1",
"electron-notarize": "^1.0.0",
"eslint": "7.18.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.4.0",
"expect-playwright": "^0.8.0",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
"find-npm-prefix": "^1.0.2",
"fs-extra": "^8.1.0",
"glob": "^7.1.6",
"jest": "^28",
"matrix-web-i18n": "^1.3.0",
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
"mkdirp": "^1.0.3",
"needle": "^2.5.0",
"node-pre-gyp": "^0.15.0",
"pacote": "^11.3.5",
"playwright": "^1.25.0",
"rimraf": "^3.0.2",
"tar": "^6.1.2",
"ts-jest": "^28.0.8",
"ts-node": "^10.4.0",
"typescript": "4.5.5"
"typescript": "^4.1.3"
},
"hakDependencies": {
"matrix-seshat": "^2.3.3",
"keytar": "^7.9.0"
},
"resolutions": {
"@types/node": "16.11.38"
"matrix-seshat": "^2.3.0",
"keytar": "^5.6.0"
},
"build": {
"appId": "im.riot.app",
"asarUnpack": "**/*.node",
"electronVersion": "13.5.2",
"files": [
"package.json",
{
@@ -133,15 +112,11 @@
"darkModeSupport": true
},
"win": {
"target": [
"squirrel",
"msi"
],
"target": {
"target": "squirrel"
},
"sign": "scripts/electron_winSign"
},
"msi": {
"perMachine": true
},
"directories": {
"output": "dist"
},
@@ -155,14 +130,5 @@
]
}
]
},
"jest": {
"testEnvironment": "node",
"testMatch": [
"<rootDir>/test/**/*-test.[jt]s?(x)"
],
"setupFilesAfterEnv": [
"expect-playwright"
]
}
}

View File

@@ -1,9 +1,12 @@
#!/bin/bash
#
# Script to perform a release of element-desktop.
#
# Requires githib-changelog-generator; to install, do
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
set -e
cd "$(dirname "$0")"
cd `dirname $0`
./node_modules/matrix-js-sdk/release.sh "$@"
./node_modules/matrix-js-sdk/release.sh -n -z "$@"

5
scripts/ci/install-deps.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -ex
yarn install --pure-lockfile $@

View File

@@ -74,14 +74,8 @@ function weblateToCounterpart(inTrs) {
if (keyParts.length === 2) {
let obj = outTrs[keyParts[0]];
if (obj === undefined) {
obj = outTrs[keyParts[0]] = {};
} else if (typeof obj === "string") {
// This is a transitional edge case if a string went from singular to pluralised and both still remain
// in the translation json file. Use the singular translation as `other` and merge pluralisation atop.
obj = outTrs[keyParts[0]] = {
"other": inTrs[key],
};
console.warn("Found entry in i18n file in both singular and pluralised form", keyParts[0]);
obj = {};
outTrs[keyParts[0]] = obj;
}
obj[keyParts[1]] = inTrs[key];
} else {

View File

@@ -1,6 +1,5 @@
const { notarize } = require('electron-notarize');
let warned = false;
exports.default = async function(context) {
const { electronPlatformName, appOutDir } = context;
const appId = context.packager.info.appInfo.id;
@@ -12,13 +11,10 @@ exports.default = async function(context) {
// from the keychain, so we need to get it from the environment.
const userId = process.env.NOTARIZE_APPLE_ID;
if (userId === undefined) {
if (!warned) {
console.log("*************************************");
console.log("* NOTARIZE_APPLE_ID is not set. *");
console.log("* This build will NOT be notarised. *");
console.log("*************************************");
warned = true;
}
console.log("*************************************");
console.log("* NOTARIZE_APPLE_ID is not set. *");
console.log("* This build will NOT be notarised. *");
console.log("*************************************");
return;
}

View File

@@ -46,19 +46,15 @@ function computeSignToolArgs(options, keyContainer) {
return args;
}
let warned = false;
exports.default = async function(options) {
const keyContainer = process.env.SIGNING_KEY_CONTAINER;
if (keyContainer === undefined) {
if (!warned) {
console.warn(
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" +
"! Skipping Windows signing. !\n" +
"! SIGNING_KEY_CONTAINER not defined. !\n" +
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
);
warned = true;
}
console.warn(
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" +
"! Skipping Windows signing. !\n" +
"! SIGNING_KEY_CONTAINER not defined. !\n" +
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
);
return;
}

114
scripts/fetch-package.js Normal file → Executable file
View File

@@ -10,13 +10,75 @@ const asar = require('asar');
const needle = require('needle');
const riotDesktopPackageJson = require('../package.json');
const { setPackageVersion } = require('./set-version.js');
const PUB_KEY_URL = "https://packages.riot.im/element-release-key.asc";
const PACKAGE_URL_PREFIX = "https://github.com/vector-im/element-web/releases/download/";
const DEVELOP_TGZ_URL = "https://element-web-develop.element.io/develop.tar.gz";
const ASAR_PATH = 'webapp.asar';
const { setPackageVersion } = require('./set-version.js');
async function getLatestDevelopUrl(bkToken) {
const buildsResult = await needle('get',
"https://api.buildkite.com/v2/organizations/matrix-dot-org/pipelines/element-web/builds",
{
branch: 'develop',
state: 'passed',
per_page: 1,
},
{
headers: {
authorization: "Bearer " + bkToken,
},
},
);
const latestBuild = buildsResult.body[0];
console.log("Latest build is " + latestBuild.number);
let artifactUrl;
for (const job of latestBuild.jobs) {
// Strip any colon-form emoji from the build name
if (job.name && job.name.replace(/:\w*:\s*/, '') === 'Package') {
artifactUrl = job.artifacts_url;
break;
}
}
if (artifactUrl === undefined) {
throw new Error("Couldn't find artifact URL - has the name of the package job changed?");
}
const artifactsResult = await needle('get', artifactUrl, {},
{
headers: {
authorization: "Bearer " + bkToken,
},
},
);
let dlUrl;
let dlFilename;
for (const artifact of artifactsResult.body) {
if (artifact.filename && /^element-.*\.tar.gz$/.test(artifact.filename)) {
dlUrl = artifact.download_url;
dlFilename = artifact.filename;
break;
}
}
if (dlUrl === undefined) {
throw new Error("Couldn't find artifact download URL - has the artifact filename changed?");
}
console.log("Fetching artifact URL...");
const dlResult = await needle('get', dlUrl, {},
{
headers: {
authorization: "Bearer " + bkToken,
},
// This URL will give us a Location header, but will also give us
// a JSON object with the direct URL. We'll take the URL and pass it
// back, then we can easily support specifying a URL directly.
follow_max: 0,
},
);
return [dlFilename, dlResult.body.url];
}
async function downloadToFile(url, filename) {
console.log("Downloading " + url + "...");
@@ -87,21 +149,24 @@ async function main() {
if (targetVersion === undefined) {
targetVersion = 'v' + riotDesktopPackageJson.version;
} else if (targetVersion !== 'develop') {
setVersion = true; // version was specified
}
if (targetVersion === 'develop') {
filename = 'develop.tar.gz';
url = DEVELOP_TGZ_URL;
verify = false; // develop builds aren't signed
} else if (targetVersion.includes("://")) {
filename = targetVersion.substring(targetVersion.lastIndexOf("/") + 1);
url = targetVersion;
verify = false; // manually verified
} else {
filename = `element-${targetVersion}.tar.gz`;
filename = 'element-' + targetVersion + '.tar.gz';
url = PACKAGE_URL_PREFIX + targetVersion + '/' + filename;
} else if (targetVersion === 'develop') {
const buildKiteApiKey = process.env.BUILDKITE_API_KEY;
if (buildKiteApiKey === undefined) {
console.log("Set BUILDKITE_API_KEY to fetch latest develop version");
console.log(
"Sorry - Buildkite's API requires authentication to access builds, " +
"even if those builds are accessible on the web with no auth.",
);
process.exit(1);
}
[filename, url] = await getLatestDevelopUrl(buildKiteApiKey);
verify = false; // develop builds aren't signed
} else {
filename = 'element-' + targetVersion + '.tar.gz';
url = PACKAGE_URL_PREFIX + targetVersion + '/' + filename;
setVersion = true;
}
const haveGpg = await new Promise((resolve) => {
@@ -143,7 +208,7 @@ async function main() {
}
let haveDeploy = false;
let expectedDeployDir = path.join(deployDir, path.basename(filename).replace(/\.tar\.gz/, ''));
const expectedDeployDir = path.join(deployDir, path.basename(filename).replace(/\.tar\.gz/, ''));
try {
await fs.opendir(expectedDeployDir);
console.log(expectedDeployDir + "already exists");
@@ -192,12 +257,6 @@ async function main() {
await tar.x({
file: outPath,
cwd: deployDir,
onentry: entry => {
// Find the appropriate extraction path, only needed for `develop` where the dir name is unknown
if (entry.type === "Directory" && !path.join(deployDir, entry.path).startsWith(expectedDeployDir)) {
expectedDeployDir = path.join(deployDir, entry.path);
}
},
});
}
@@ -221,7 +280,7 @@ async function main() {
await asar.createPackage(expectedDeployDir, ASAR_PATH);
if (setVersion) {
const semVer = fs.readFileSync(path.join(expectedDeployDir, "version"), "utf-8").trim();
const semVer = targetVersion.slice(1);
console.log("Updating version to " + semVer);
await setPackageVersion(semVer);
}
@@ -229,9 +288,4 @@ async function main() {
console.log("Done!");
}
main().then((ret) => {
process.exit(ret);
}).catch(e => {
console.error(e);
process.exit(1);
});
main().then((ret) => process.exit(ret)).catch(e => process.exit(1));

View File

@@ -17,8 +17,7 @@ clone() {
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
git clone git://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0
fi
}

View File

@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DependencyInfo } from "./dep";
import HakEnv from "./hakEnv";
export default async function build(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function build(hakEnv, moduleInfo) {
await moduleInfo.scripts.build(hakEnv, moduleInfo);
}
module.exports = build;

View File

@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DependencyInfo } from "./dep";
import HakEnv from "./hakEnv";
export default async function check(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function check(hakEnv, moduleInfo) {
if (moduleInfo.scripts.check) {
await moduleInfo.scripts.check(hakEnv, moduleInfo);
}
}
module.exports = check;

View File

@@ -14,15 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import rimraf from 'rimraf';
const path = require('path');
import { DependencyInfo } from './dep';
import HakEnv from './hakEnv';
const rimraf = require('rimraf');
export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
await new Promise<void>((resolve, reject) => {
rimraf(moduleInfo.moduleDotHakDir, (err: Error) => {
async function clean(hakEnv, moduleInfo) {
await new Promise((resolve, reject) => {
rimraf(moduleInfo.moduleDotHakDir, (err) => {
if (err) {
reject(err);
} else {
@@ -31,8 +29,8 @@ export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
await new Promise<void>((resolve, reject) => {
rimraf(path.join(hakEnv.dotHakDir, 'links', moduleInfo.name), (err: Error) => {
await new Promise((resolve, reject) => {
rimraf(path.join(hakEnv.dotHakDir, 'links', moduleInfo.name), (err) => {
if (err) {
reject(err);
} else {
@@ -41,8 +39,8 @@ export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
await new Promise<void>((resolve, reject) => {
rimraf(path.join(hakEnv.projectRoot, 'node_modules', moduleInfo.name), (err: Error) => {
await new Promise((resolve, reject) => {
rimraf(path.join(hakEnv.projectRoot, 'node_modules', moduleInfo.name), (err) => {
if (err) {
reject(err);
} else {
@@ -51,3 +49,5 @@ export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
}
module.exports = clean;

View File

@@ -14,26 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import fsProm from 'fs/promises';
import childProcess from 'child_process';
import rimraf from 'rimraf';
import glob from 'glob';
import mkdirp from 'mkdirp';
const path = require('path');
const fsProm = require('fs').promises;
const childProcess = require('child_process');
import HakEnv from './hakEnv';
import { DependencyInfo } from './dep';
const rimraf = require('rimraf');
const glob = require('glob');
const mkdirp = require('mkdirp');
export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function copy(hakEnv, moduleInfo) {
if (moduleInfo.cfg.prune) {
console.log("Removing " + moduleInfo.cfg.prune + " from " + moduleInfo.moduleOutDir);
// rimraf doesn't have a 'cwd' option: it always uses process.cwd()
// (and if you set glob.cwd it just breaks because it can't find the files)
const oldCwd = process.cwd();
try {
await mkdirp(moduleInfo.moduleOutDir);
process.chdir(moduleInfo.moduleOutDir);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
rimraf(moduleInfo.cfg.prune, {}, err => {
err ? reject(err) : resolve();
});
@@ -47,7 +44,7 @@ export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo):
// If there are multiple moduleBuildDirs, singular moduleBuildDir
// is the same as moduleBuildDirs[0], so we're just listing the contents
// of the first one.
const files = await new Promise<string[]>((resolve, reject) => {
const files = await new Promise(async (resolve, reject) => {
glob(moduleInfo.cfg.copy, {
nosort: true,
silent: true,
@@ -71,7 +68,7 @@ export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo):
const dst = path.join(moduleInfo.moduleOutDir, f);
await mkdirp(path.dirname(dst));
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
childProcess.execFile('lipo',
['-create', '-output', dst, ...components], (err) => {
if (err) {
@@ -99,3 +96,5 @@ export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo):
}
}
}
module.exports = copy;

View File

@@ -1,32 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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 HakEnv from "./hakEnv";
export interface DependencyInfo {
name: string;
version: string;
cfg: Record<string, any>;
moduleHakDir: string;
moduleDotHakDir: string;
moduleTargetDotHakDir: string;
moduleBuildDir: string;
moduleBuildDirs: string[];
moduleOutDir: string;
nodeModuleBinDir: string;
depPrefix: string;
scripts: Record<string, (hakEnv: HakEnv, moduleInfo: DependencyInfo) => Promise<void> >;
}

View File

@@ -14,14 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fsProm from 'fs/promises';
import childProcess from 'child_process';
import pacote from 'pacote';
const fsProm = require('fs').promises;
const childProcess = require('child_process');
import HakEnv from './hakEnv';
import { DependencyInfo } from './dep';
const pacote = require('pacote');
export default async function fetch(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function fetch(hakEnv, moduleInfo) {
let haveModuleBuildDir;
try {
const stats = await fsProm.stat(moduleInfo.moduleBuildDir);
@@ -40,7 +38,7 @@ export default async function fetch(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
console.log("Running yarn install in " + moduleInfo.moduleBuildDir);
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(
hakEnv.isWin() ? 'yarn.cmd' : 'yarn',
['install', '--ignore-scripts'],
@@ -68,3 +66,5 @@ export default async function fetch(hakEnv: HakEnv, moduleInfo: DependencyInfo):
packumentCache,
});
}
module.exports = fetch;

View File

@@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import mkdirp from 'mkdirp';
const mkdirp = require('mkdirp');
import { DependencyInfo } from './dep';
import HakEnv from './hakEnv';
export default async function fetchDeps(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function fetchDeps(hakEnv, moduleInfo) {
await mkdirp(moduleInfo.moduleDotHakDir);
if (moduleInfo.scripts.fetchDeps) {
await moduleInfo.scripts.fetchDeps(hakEnv, moduleInfo);
}
}
module.exports = fetchDeps;

View File

@@ -14,20 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import os from 'os';
import nodePreGypVersioning from "node-pre-gyp/lib/util/versioning";
import { getElectronVersion } from "app-builder-lib/out/electron/electronVersion";
const path = require('path');
const os = require('os');
import { Arch, Target, TARGETS, getHost, isHostId, TargetId } from './target';
const nodePreGypVersioning = require('node-pre-gyp/lib/util/versioning');
const { TARGETS, getHost, isHostId } = require('./target');
async function getRuntime(projectRoot: string): Promise<string> {
const electronVersion = await getElectronVersion(projectRoot);
function getElectronVersion(packageJson) {
// should we pick the version of an installed electron
// dependency, and if so, before or after electronVersion?
if (packageJson.build && packageJson.build.electronVersion) {
return packageJson.build.electronVersion;
}
return null;
}
function getRuntime(packageJson) {
const electronVersion = getElectronVersion(packageJson);
return electronVersion ? 'electron' : 'node-webkit';
}
async function getRuntimeVersion(projectRoot: string): Promise<string> {
const electronVersion = await getElectronVersion(projectRoot);
function getRuntimeVersion(packageJson) {
const electronVersion = getElectronVersion(packageJson);
if (electronVersion) {
return electronVersion;
} else {
@@ -35,31 +43,32 @@ async function getRuntimeVersion(projectRoot: string): Promise<string> {
}
}
export default class HakEnv {
public readonly target: Target;
public runtime: string;
public runtimeVersion: string;
public dotHakDir: string;
constructor(public readonly projectRoot: string, targetId: TargetId | null) {
module.exports = class HakEnv {
constructor(prefix, packageJson, targetId) {
let target;
if (targetId) {
this.target = TARGETS[targetId];
target = TARGETS[targetId];
} else {
this.target = getHost();
target = getHost();
}
if (!this.target) {
if (!target) {
throw new Error(`Unknown target ${targetId}!`);
}
this.dotHakDir = path.join(this.projectRoot, '.hak');
Object.assign(this, {
// what we're targeting
runtime: getRuntime(packageJson),
runtimeVersion: getRuntimeVersion(packageJson),
target,
// paths
projectRoot: prefix,
dotHakDir: path.join(prefix, '.hak'),
});
}
public async init() {
this.runtime = await getRuntime(this.projectRoot);
this.runtimeVersion = await getRuntimeVersion(this.projectRoot);
}
public getRuntimeAbi(): string {
getRuntimeAbi() {
return nodePreGypVersioning.get_runtime_abi(
this.runtime,
this.runtimeVersion,
@@ -67,39 +76,39 @@ export default class HakEnv {
}
// {node_abi}-{platform}-{arch}
public getNodeTriple(): string {
getNodeTriple() {
return this.getRuntimeAbi() + '-' + this.target.platform + '-' + this.target.arch;
}
public getTargetId(): TargetId {
getTargetId() {
return this.target.id;
}
public isWin(): boolean {
isWin() {
return this.target.platform === 'win32';
}
public isMac(): boolean {
isMac() {
return this.target.platform === 'darwin';
}
public isLinux(): boolean {
isLinux() {
return this.target.platform === 'linux';
}
public getTargetArch(): Arch {
getTargetArch() {
return this.target.arch;
}
public isHost(): boolean {
isHost() {
return isHostId(this.target.id);
}
public makeGypEnv(): Record<string, string> {
makeGypEnv() {
return Object.assign({}, process.env, {
npm_config_arch: this.target.arch,
npm_config_target_arch: this.target.arch,
npm_config_disturl: 'https://electronjs.org/headers',
npm_config_disturl: 'https://atom.io/download/electron',
npm_config_runtime: this.runtime,
npm_config_target: this.runtimeVersion,
npm_config_build_from_source: true,
@@ -107,15 +116,7 @@ export default class HakEnv {
});
}
public getNodeModuleBin(name: string): string {
getNodeModuleBin(name) {
return path.join(this.projectRoot, 'node_modules', '.bin', name);
}
public wantsStaticSqlCipherUnix(): boolean {
return this.isMac() || process.env.SQLCIPHER_STATIC == '1';
}
public wantsStaticSqlCipher(): boolean {
return this.isWin() || this.wantsStaticSqlCipherUnix();
}
}
};

View File

@@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import findNpmPrefix from 'find-npm-prefix';
const path = require('path');
import HakEnv from './hakEnv';
import { TargetId } from './target';
import { DependencyInfo } from './dep';
const findNpmPrefix = require('find-npm-prefix');
const HakEnv = require('./hakEnv');
const GENERALCOMMANDS = [
'target',
@@ -61,7 +60,7 @@ async function main() {
process.exit(1);
}
const targetIds: TargetId[] = [];
const targetIds = [];
// Apply `--target <target>` option if specified
// Can be specified multiple times for the copy command to bundle
// multiple archs into a single universal output module)
@@ -74,23 +73,20 @@ async function main() {
process.exit(1);
}
// Extract target ID and remove from args
targetIds.push(process.argv.splice(targetIndex, 2)[1] as TargetId);
targetIds.push(process.argv.splice(targetIndex, 2)[1]);
}
const hakEnvs = targetIds.map(tid => new HakEnv(prefix, tid));
if (hakEnvs.length == 0) hakEnvs.push(new HakEnv(prefix, null));
for (const h of hakEnvs) {
await h.init();
}
const hakEnvs = targetIds.map(tid => new HakEnv(prefix, packageJson, tid));
if (hakEnvs.length == 0) hakEnvs.push(new HakEnv(prefix, packageJson, null));
const hakEnv = hakEnvs[0];
const deps: Record<string, DependencyInfo> = {};
const deps = {};
const hakDepsCfg = packageJson.hakDependencies || {};
for (const dep of Object.keys(hakDepsCfg)) {
const hakJsonPath = path.join(prefix, 'hak', dep, 'hak.json');
let hakJson: Record<string, any>;
let hakJson;
try {
hakJson = await require(hakJsonPath);
} catch (e) {
@@ -115,17 +111,12 @@ async function main() {
for (const s of HAKSCRIPTS) {
if (hakJson.scripts && hakJson.scripts[s]) {
const scriptModule = await import(path.join(prefix, 'hak', dep, hakJson.scripts[s]));
if (scriptModule.__esModule) {
deps[dep].scripts[s] = scriptModule.default;
} else {
deps[dep].scripts[s] = scriptModule;
}
deps[dep].scripts[s] = require(path.join(prefix, 'hak', dep, hakJson.scripts[s]));
}
}
}
let cmds: string[];
let cmds;
if (process.argv.length < 3) {
cmds = ['check', 'fetch', 'fetchDeps', 'build', 'copy', 'link'];
} else if (METACOMMANDS[process.argv[2]]) {
@@ -160,7 +151,7 @@ async function main() {
process.exit(1);
}
const cmdFunc = (await import('./' + cmd)).default;
const cmdFunc = require('./' + cmd);
for (const mod of modules) {
const depInfo = deps[mod];

View File

@@ -14,15 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import os from 'os';
import fsProm from 'fs/promises';
import childProcess from 'child_process';
const path = require('path');
const os = require('os');
const fsProm = require('fs').promises;
const childProcess = require('child_process');
import HakEnv from './hakEnv';
import { DependencyInfo } from './dep';
export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
async function link(hakEnv, moduleInfo) {
const yarnrc = path.join(hakEnv.projectRoot, '.yarnrc');
// this is fairly terrible but it's reasonably clunky to either parse a yarnrc
// properly or get yarn to do it, so this will probably suffice for now.
@@ -49,7 +46,7 @@ export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo):
const yarnCmd = 'yarn' + (hakEnv.isWin() ? '.cmd' : '');
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(yarnCmd, ['link'], {
cwd: moduleInfo.moduleOutDir,
stdio: 'inherit',
@@ -59,7 +56,7 @@ export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
await new Promise<void>((resolve, reject) => {
await new Promise((resolve, reject) => {
const proc = childProcess.spawn(yarnCmd, ['link', moduleInfo.name], {
cwd: hakEnv.projectRoot,
stdio: 'inherit',
@@ -69,3 +66,5 @@ export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo):
});
});
}
module.exports = link;

82
scripts/hak/target.js Normal file
View File

@@ -0,0 +1,82 @@
"use strict";
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
/*
* THIS FILE IS GENERATED, NOT MEANT FOR EDITING DIRECTLY
* The original source is `target.ts` in the `element-builder` repo. You can
* edit it over there, run `yarn build`, and paste the changes here. It is
* currently assumed this file will rarely change, so a spearate package is not
* yet warranted.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isHost = exports.isHostId = exports.getHost = exports.ENABLED_TARGETS = exports.TARGETS = void 0;
const aarch64AppleDarwin = {
id: 'aarch64-apple-darwin',
platform: 'darwin',
arch: 'arm64',
};
const i686PcWindowsMsvc = {
id: 'i686-pc-windows-msvc',
platform: 'win32',
arch: 'ia32',
vcVarsArch: 'x86',
};
const x8664PcWindowsMsvc = {
id: 'x86_64-pc-windows-msvc',
platform: 'win32',
arch: 'x64',
vcVarsArch: 'amd64',
};
const x8664AppleDarwin = {
id: 'x86_64-apple-darwin',
platform: 'darwin',
arch: 'x64',
};
const x8664UnknownLinuxGnu = {
id: 'x86_64-unknown-linux-gnu',
platform: 'linux',
arch: 'x64',
};
exports.TARGETS = {
'aarch64-apple-darwin': aarch64AppleDarwin,
'i686-pc-windows-msvc': i686PcWindowsMsvc,
'x86_64-pc-windows-msvc': x8664PcWindowsMsvc,
'x86_64-apple-darwin': x8664AppleDarwin,
'x86_64-unknown-linux-gnu': x8664UnknownLinuxGnu,
};
// The set of targets we build by default, sorted by increasing complexity so
// that we fail fast when the native host target fails.
exports.ENABLED_TARGETS = [
exports.TARGETS['x86_64-apple-darwin'],
exports.TARGETS['aarch64-apple-darwin'],
exports.TARGETS['x86_64-unknown-linux-gnu'],
exports.TARGETS['i686-pc-windows-msvc'],
];
function getHost() {
return Object.values(exports.TARGETS).find(target => (target.platform === process.platform &&
target.arch === process.arch));
}
exports.getHost = getHost;
function isHostId(id) {
return getHost()?.id === id;
}
exports.isHostId = isHostId;
function isHost(target) {
return getHost()?.id === target.id;
}
exports.isHost = isHost;

View File

@@ -1,196 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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 { GLIBC, MUSL, family as processLibC } from "detect-libc";
// We borrow Rust's target naming scheme as a way of expressing all target
// details in a single string.
// See https://doc.rust-lang.org/rustc/platform-support.html.
export type TargetId =
'aarch64-apple-darwin' |
'x86_64-apple-darwin' |
'universal-apple-darwin' |
'i686-pc-windows-msvc' |
'x86_64-pc-windows-msvc' |
'i686-unknown-linux-musl' |
'i686-unknown-linux-gnu' |
'x86_64-unknown-linux-musl' |
'x86_64-unknown-linux-gnu' |
'aarch64-unknown-linux-musl' |
'aarch64-unknown-linux-gnu' |
'powerpc64le-unknown-linux-musl' |
'powerpc64le-unknown-linux-gnu';
// Values are expected to match those used in `process.platform`.
export type Platform = 'darwin' | 'linux' | 'win32';
// Values are expected to match those used in `process.arch`.
export type Arch = 'arm64' | 'ia32' | 'x64' | 'ppc64' | 'universal';
// Values are expected to match those used by Visual Studio's `vcvarsall.bat`.
// See https://docs.microsoft.com/cpp/build/building-on-the-command-line?view=msvc-160#vcvarsall-syntax
export type VcVarsArch = 'amd64' | 'arm64' | 'x86';
export type Target = {
id: TargetId;
platform: Platform;
arch: Arch;
};
export type WindowsTarget = Target & {
platform: 'win32';
vcVarsArch: VcVarsArch;
};
export type LinuxTarget = Target & {
platform: 'linux';
libC: typeof processLibC;
};
export type UniversalTarget = Target & {
arch: 'universal';
subtargets: Target[];
};
const aarch64AppleDarwin: Target = {
id: 'aarch64-apple-darwin',
platform: 'darwin',
arch: 'arm64',
};
const x8664AppleDarwin: Target = {
id: 'x86_64-apple-darwin',
platform: 'darwin',
arch: 'x64',
};
const universalAppleDarwin: UniversalTarget = {
id: 'universal-apple-darwin',
platform: 'darwin',
arch: 'universal',
subtargets: [
aarch64AppleDarwin,
x8664AppleDarwin,
],
};
const i686PcWindowsMsvc: WindowsTarget = {
id: 'i686-pc-windows-msvc',
platform: 'win32',
arch: 'ia32',
vcVarsArch: 'x86',
};
const x8664PcWindowsMsvc: WindowsTarget = {
id: 'x86_64-pc-windows-msvc',
platform: 'win32',
arch: 'x64',
vcVarsArch: 'amd64',
};
const x8664UnknownLinuxGnu: LinuxTarget = {
id: 'x86_64-unknown-linux-gnu',
platform: 'linux',
arch: 'x64',
libC: GLIBC,
};
const x8664UnknownLinuxMusl: LinuxTarget = {
id: 'x86_64-unknown-linux-musl',
platform: 'linux',
arch: 'x64',
libC: MUSL,
};
const i686UnknownLinuxGnu: LinuxTarget = {
id: 'i686-unknown-linux-gnu',
platform: 'linux',
arch: 'ia32',
libC: GLIBC,
};
const i686UnknownLinuxMusl: LinuxTarget = {
id: 'i686-unknown-linux-musl',
platform: 'linux',
arch: 'ia32',
libC: MUSL,
};
const aarch64UnknownLinuxGnu: LinuxTarget = {
id: 'aarch64-unknown-linux-gnu',
platform: 'linux',
arch: 'arm64',
libC: GLIBC,
};
const aarch64UnknownLinuxMusl: LinuxTarget = {
id: 'aarch64-unknown-linux-musl',
platform: 'linux',
arch: 'arm64',
libC: MUSL,
};
const powerpc64leUnknownLinuxGnu: LinuxTarget = {
id: 'powerpc64le-unknown-linux-gnu',
platform: 'linux',
arch: 'ppc64',
libC: GLIBC,
};
const powerpc64leUnknownLinuxMusl: LinuxTarget = {
id: 'powerpc64le-unknown-linux-musl',
platform: 'linux',
arch: 'ppc64',
libC: MUSL,
};
export const TARGETS: Record<TargetId, Target> = {
// macOS
'aarch64-apple-darwin': aarch64AppleDarwin,
'x86_64-apple-darwin': x8664AppleDarwin,
'universal-apple-darwin': universalAppleDarwin,
// Windows
'i686-pc-windows-msvc': i686PcWindowsMsvc,
'x86_64-pc-windows-msvc': x8664PcWindowsMsvc,
// Linux
'i686-unknown-linux-musl': i686UnknownLinuxMusl,
'i686-unknown-linux-gnu': i686UnknownLinuxGnu,
'x86_64-unknown-linux-musl': x8664UnknownLinuxMusl,
'x86_64-unknown-linux-gnu': x8664UnknownLinuxGnu,
'aarch64-unknown-linux-musl': aarch64UnknownLinuxMusl,
'aarch64-unknown-linux-gnu': aarch64UnknownLinuxGnu,
'powerpc64le-unknown-linux-musl': powerpc64leUnknownLinuxMusl,
'powerpc64le-unknown-linux-gnu': powerpc64leUnknownLinuxGnu,
};
export function getHost(): Target | undefined {
return Object.values(TARGETS).find(target => (
target.platform === process.platform &&
target.arch === process.arch &&
(
process.platform !== 'linux' ||
(target as LinuxTarget).libC === processLibC
)
));
}
export function isHostId(id: TargetId): boolean {
return getHost()?.id === id;
}
export function isHost(target: Target): boolean {
return getHost()?.id === target.id;
}

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"target": "es2017",
"module": "commonjs",
"sourceMap": false,
"lib": [
"es2019",
]
},
"include": [
"./**/*.ts"
],
"ts-node": {
"transpileOnly": true
}
}

View File

@@ -1,10 +1,8 @@
#!/bin/bash
IMAGE=${DOCKER_IMAGE_NAME:-"element-desktop-dockerbuild"}
docker inspect "$IMAGE" 2> /dev/null > /dev/null
docker inspect element-desktop-dockerbuild 2> /dev/null > /dev/null
if [ $? != 0 ]; then
echo "Docker image $IMAGE not found. Have you run yarn run docker:setup?"
echo "Docker image element-desktop-dockerbuild not found. Have you run yarn run docker:setup?"
exit 1
fi
@@ -20,4 +18,4 @@ docker run --rm -ti \
-v ${PWD}/docker/.gnupg:/root/.gnupg \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
"$IMAGE" "$@"
element-desktop-dockerbuild "$@"

View File

@@ -1,5 +1,5 @@
/*
Copyright 2021 - 2022 New Vector Ltd
Copyright 2021 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.
@@ -15,32 +15,12 @@ limitations under the License.
*/
import { BrowserWindow } from "electron";
import Store from "electron-store";
import AutoLaunch from "auto-launch";
import { AppLocalization } from "../language-helper";
declare global {
namespace NodeJS {
interface Global {
mainWindow: BrowserWindow;
appQuitting: boolean;
appLocalization: AppLocalization;
launcher: AutoLaunch;
vectorConfig: Record<string, any>;
trayConfig: {
// eslint-disable-next-line camelcase
icon_path: string;
brand: string;
};
store: Store<{
warnBeforeExit?: boolean;
minimizeToTray?: boolean;
spellCheckerEnabled?: boolean;
autoHideMenuBar?: boolean;
locale?: string | string[];
disableHardwareAcceleration?: boolean;
}>;
}
}
}

View File

@@ -1,54 +0,0 @@
// Based on https://github.com/atom/node-keytar/blob/master/keytar.d.ts because keytar is a hak-dependency and not a normal one
// Definitions by: Milan Burda <https://github.com/miniak>, Brendan Forster <https://github.com/shiftkey>, Hari Juturu <https://github.com/juturu>
// Adapted from DefinitelyTyped: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/keytar/index.d.ts
declare module "keytar" {
/**
* Get the stored password for the service and account.
*
* @param service The string service name.
* @param account The string account name.
*
* @returns A promise for the password string.
*/
export function getPassword(service: string, account: string): Promise<string | null>;
/**
* Add the password for the service and account to the keychain.
*
* @param service The string service name.
* @param account The string account name.
* @param password The string password.
*
* @returns A promise for the set password completion.
*/
export function setPassword(service: string, account: string, password: string): Promise<void>;
/**
* Delete the stored password for the service and account.
*
* @param service The string service name.
* @param account The string account name.
*
* @returns A promise for the deletion status. True on success.
*/
export function deletePassword(service: string, account: string): Promise<boolean>;
/**
* Find a password for the service in the keychain.
*
* @param service The string service name.
*
* @returns A promise for the password string.
*/
export function findPassword(service: string): Promise<string | null>;
/**
* Find all accounts and passwords for `service` in the keychain.
*
* @param service The string service name.
*
* @returns A promise for the array of found credentials.
*/
export function findCredentials(service: string): Promise<Array<{ account: string, password: string}>>;
}

View File

@@ -1,145 +0,0 @@
/*
Copyright 2022 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.
*/
declare module "matrix-seshat" {
interface IConfig {
language?: string;
passphrase?: string;
}
/* eslint-disable camelcase */
interface IMatrixEvent {
event_id: string;
sender: string;
room_id: string;
origin_server_ts: number;
content: Record<string, any>;
}
interface IMatrixProfile {
displayname?: string;
avatar_url?: string;
}
interface ISearchArgs {
searchTerm: number;
limit: number;
before_limit: number;
after_limit: number;
order_by_recency: boolean;
next_batch?: string;
}
interface ISearchContext {
events_before: IMatrixEvent[];
events_after: IMatrixEvent[];
profile_info: { [userId: string]: IMatrixProfile };
}
interface ISearchResult {
next_batch: string;
count: number;
results: Array<{
rank: number;
result: IMatrixEvent;
context: ISearchContext;
}>;
}
/* eslint-enable camelcase */
interface ICheckpoint {
roomId: string;
token: string;
fullCrawl: boolean;
direction: "b" | "f";
}
interface IDatabaseStats {
size: number;
eventCount: number;
roomCount: number;
}
interface ILoadArgs {
roomId: string;
limit: number;
fromEvent: string;
direction: "b" | "f";
}
interface ILoadResult {
event: IMatrixEvent;
matrixProfile: IMatrixProfile;
}
export class Seshat {
constructor(path: string, config?: IConfig);
public addEvent(matrixEvent: IMatrixEvent, profile?: IMatrixProfile): void;
public deleteEvent(eventId: string): Promise<boolean>;
public commit(force?: boolean): Promise<number>;
public commitSync(wait?: boolean, force?: boolean): number;
public reload(): void;
public search(args: ISearchArgs): Promise<ISearchResult>;
public searchSync(
term: string,
limit?: number,
beforeLimit?: number,
afterLimit?: number,
orderByRecency?: boolean,
): ISearchResult;
public addHistoricEventsSync(
events: IMatrixEvent[],
newCheckpoint?: ICheckpoint,
oldCheckpoint?: ICheckpoint,
): boolean;
public addHistoricEvents(
events: IMatrixEvent[],
newCheckpoint?: ICheckpoint,
oldCheckpoint?: ICheckpoint,
): Promise<boolean>;
public addCrawlerCheckpoint(checkpoint: ICheckpoint): Promise<void>;
public removeCrawlerCheckpoint(checkpoint: ICheckpoint): Promise<void>;
public loadCheckpoints(): Promise<ICheckpoint[]>;
public getSize(): Promise<number>;
public getStats(): Promise<IDatabaseStats>;
public delete(): Promise<void>;
public shutdown(): Promise<void>;
public changePassphrase(newPassphrase: string): Promise<void>;
public isEmpty(): Promise<boolean>;
public isRoomIndexed(roomId: string): Promise<boolean>;
public getUserVersion(): Promise<number>;
public setUserVersion(version: number): Promise<void>;
public loadFileEvents(args: ILoadArgs): Promise<ILoadResult[]>;
}
interface IRecoveryInfo {
totalEvents: number;
reindexedEvents: number;
done: number;
}
export class SeshatRecovery {
constructor(path: string, config?: IConfig);
public info(): IRecoveryInfo;
public getUserVersion(): Promise<number>;
public shutdown(): Promise<void>;
public reindex(): Promise<void>;
}
export class ReindexError extends Error {
constructor(message?: string);
}
}

View File

@@ -19,43 +19,68 @@ limitations under the License.
// Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
import "./squirrelhooks";
import {
app,
BrowserWindow,
Menu,
autoUpdater,
protocol,
dialog,
} from "electron";
import { app, ipcMain, powerSaveBlocker, BrowserWindow, Menu, autoUpdater, protocol, dialog } from "electron";
import AutoLaunch from "auto-launch";
import path from "path";
import windowStateKeeper from 'electron-window-state';
import Store from 'electron-store';
import fs, { promises as afs } from "fs";
import crypto from "crypto";
import { URL } from "url";
import minimist from "minimist";
import "./ipc";
import "./keytar";
import "./seshat";
import "./settings";
import * as tray from "./tray";
import { buildMenuTemplate } from './vectormenu';
import webContentsHandler from './webcontents-handler';
import * as updater from './updater';
import { getProfileFromDeeplink, protocolInit } from './protocol';
import { getProfileFromDeeplink, protocolInit, recordSSOSession } from './protocol';
import { _t, AppLocalization } from './language-helper';
import Input = Electron.Input;
const argv = minimist(process.argv, {
alias: { help: "h" },
});
let keytar;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
keytar = require('keytar');
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
console.log("Keytar isn't installed; secure key storage is disabled.");
} else {
console.warn("Keytar unexpected error:", e);
}
}
let seshatSupported = false;
let Seshat;
let SeshatRecovery;
let ReindexError;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const seshatModule = require('matrix-seshat');
Seshat = seshatModule.Seshat;
SeshatRecovery = seshatModule.SeshatRecovery;
ReindexError = seshatModule.ReindexError;
seshatSupported = true;
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
console.log("Seshat isn't installed, event indexing is disabled.");
} else {
console.warn("Seshat unexpected error:", e);
}
}
// Things we need throughout the file but need to be created
// async to are initialised in setupGlobals()
let asarPath: string;
let resPath: string;
let iconPath: string;
let asarPath;
let resPath;
let vectorConfig;
let iconPath;
let trayConfig;
let launcher;
let appLocalization;
if (argv["help"]) {
console.log("Options:");
@@ -73,12 +98,12 @@ if (argv["help"]) {
// Electron creates the user data directory (with just an empty 'Dictionaries' directory...)
// as soon as the app path is set, so pick a random path in it that must exist if it's a
// real user data directory.
function isRealUserDataDir(d: string): boolean {
function isRealUserDataDir(d) {
return fs.existsSync(path.join(d, 'IndexedDB'));
}
// check if we are passed a profile in the SSO callback url
let userDataPath: string;
let userDataPath;
const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]);
if (userDataPathInProtocol) {
@@ -108,7 +133,7 @@ if (userDataPathInProtocol) {
}
app.setPath('userData', userDataPath);
async function tryPaths(name: string, root: string, rawPaths: string[]): Promise<string> {
async function tryPaths(name, root, rawPaths) {
// Make everything relative to root
const paths = rawPaths.map(p => path.join(root, p));
@@ -127,7 +152,7 @@ async function tryPaths(name: string, root: string, rawPaths: string[]): Promise
}
// Find the webapp resources and set up things that require them
async function setupGlobals(): Promise<void> {
async function setupGlobals() {
// find the webapp asar.
asarPath = await tryPaths("webapp", __dirname, [
// If run from the source checkout, this will be in the directory above
@@ -151,13 +176,13 @@ async function setupGlobals(): Promise<void> {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
global.vectorConfig = require(asarPath + 'config.json');
vectorConfig = require(asarPath + 'config.json');
} catch (e) {
// it would be nice to check the error code here and bail if the config
// is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
// file or invalid json, so node is just very unhelpful.
// Continue with the defaults (ie. an empty config)
global.vectorConfig = {};
vectorConfig = {};
}
try {
@@ -171,19 +196,19 @@ async function setupGlobals(): Promise<void> {
const homeserverProps = ['default_is_url', 'default_hs_url', 'default_server_name', 'default_server_config'];
if (Object.keys(localConfig).find(k => homeserverProps.includes(k))) {
// Rip out all the homeserver options from the vector config
global.vectorConfig = Object.keys(global.vectorConfig)
vectorConfig = Object.keys(vectorConfig)
.filter(k => !homeserverProps.includes(k))
.reduce((obj, key) => {obj[key] = global.vectorConfig[key]; return obj;}, {});
.reduce((obj, key) => {obj[key] = vectorConfig[key]; return obj;}, {});
}
global.vectorConfig = Object.assign(global.vectorConfig, localConfig);
vectorConfig = Object.assign(vectorConfig, localConfig);
} catch (e) {
if (e instanceof SyntaxError) {
dialog.showMessageBox({
type: "error",
title: `Your ${global.vectorConfig.brand || 'Element'} is misconfigured`,
message: `Your custom ${global.vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${global.vectorConfig.brand || 'Element'}.`,
title: `Your ${vectorConfig.brand || 'Element'} is misconfigured`,
message: `Your custom ${vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${vectorConfig.brand || 'Element'}.`,
detail: e.message || "",
});
}
@@ -195,14 +220,14 @@ async function setupGlobals(): Promise<void> {
// It's important to call `path.join` so we don't end up with the packaged asar in the final path.
const iconFile = `element.${process.platform === 'win32' ? 'ico' : 'png'}`;
iconPath = path.join(resPath, "img", iconFile);
global.trayConfig = {
trayConfig = {
icon_path: iconPath,
brand: global.vectorConfig.brand || 'Element',
brand: vectorConfig.brand || 'Element',
};
// launcher
global.launcher = new AutoLaunch({
name: global.vectorConfig.brand || 'Element',
launcher = new AutoLaunch({
name: vectorConfig.brand || 'Element',
isHidden: true,
mac: {
useLaunchAgent: true,
@@ -210,10 +235,10 @@ async function setupGlobals(): Promise<void> {
});
}
async function moveAutoLauncher(): Promise<void> {
async function moveAutoLauncher() {
// Look for an auto-launcher under 'Riot' and if we find one, port it's
// enabled/disabled-ness over to the new 'Element' launcher
if (!global.vectorConfig.brand || global.vectorConfig.brand === 'Element') {
// enabled/disbaledp-ness over to the new 'Element' launcher
if (!vectorConfig.brand || vectorConfig.brand === 'Element') {
const oldLauncher = new AutoLaunch({
name: 'Riot',
isHidden: true,
@@ -224,32 +249,40 @@ async function moveAutoLauncher(): Promise<void> {
const wasEnabled = await oldLauncher.isEnabled();
if (wasEnabled) {
await oldLauncher.disable();
await global.launcher.enable();
await launcher.enable();
}
}
}
global.store = new Store({ name: "electron-config" });
const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
const store = new Store<{
warnBeforeExit?: boolean;
minimizeToTray?: boolean;
spellCheckerEnabled?: boolean;
autoHideMenuBar?: boolean;
locale?: string | string[];
}>({ name: "electron-config" });
let eventIndex = null;
let mainWindow = null;
global.appQuitting = false;
const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
const exitShortcuts = [
(input, platform) => platform !== 'darwin' && input.alt && input.key.toUpperCase() === 'F4',
(input, platform) => platform !== 'darwin' && input.control && input.key.toUpperCase() === 'Q',
(input, platform) => platform === 'darwin' && input.meta && input.key.toUpperCase() === 'Q',
];
const warnBeforeExit = (event: Event, input: Input): void => {
const shouldWarnBeforeExit = global.store.get('warnBeforeExit', true);
const warnBeforeExit = (event, input) => {
const shouldWarnBeforeExit = store.get('warnBeforeExit', true);
const exitShortcutPressed =
input.type === 'keyDown' && exitShortcuts.some(shortcutFn => shortcutFn(input, process.platform));
if (shouldWarnBeforeExit && exitShortcutPressed) {
const shouldCancelCloseRequest = dialog.showMessageBoxSync(global.mainWindow, {
const shouldCancelCloseRequest = dialog.showMessageBoxSync(mainWindow, {
type: "question",
buttons: [_t("Cancel"), _t("Close %(brand)s", {
brand: global.vectorConfig.brand || 'Element',
})],
buttons: [_t("Cancel"), _t("Close Element")],
message: _t("Are you sure you want to quit?"),
defaultId: 1,
cancelId: 0,
@@ -261,16 +294,496 @@ const warnBeforeExit = (event: Event, input: Input): void => {
}
};
const deleteContents = async (p) => {
for (const entry of await afs.readdir(p)) {
const curPath = path.join(p, entry);
await afs.unlink(curPath);
}
};
async function randomArray(size) {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, buf) => {
if (err) {
reject(err);
} else {
resolve(buf.toString("base64").replace(/=+$/g, ''));
}
});
});
}
// handle uncaught errors otherwise it displays
// stack traces in popup dialogs, which is terrible (which
// it will do any time the auto update poke fails, and there's
// no other way to catch this error).
// Assuming we generally run from the console when developing,
// this is far preferable.
process.on('uncaughtException', function(error: Error): void {
process.on('uncaughtException', function(error) {
console.log('Unhandled exception', error);
});
let focusHandlerAttached = false;
ipcMain.on('setBadgeCount', function(ev, count) {
if (process.platform !== 'win32') {
// only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron
// has some Windows support too, and in some Windows environments this leads to two badges rendering atop
// each other. See https://github.com/vector-im/element-web/issues/16942
app.badgeCount = count;
}
if (count === 0 && mainWindow) {
mainWindow.flashFrame(false);
}
});
ipcMain.on('loudNotification', function() {
if (process.platform === 'win32' && mainWindow && !mainWindow.isFocused() && !focusHandlerAttached) {
mainWindow.flashFrame(true);
mainWindow.once('focus', () => {
mainWindow.flashFrame(false);
focusHandlerAttached = false;
});
focusHandlerAttached = true;
}
});
let powerSaveBlockerId = null;
ipcMain.on('app_onAction', function(ev, payload) {
switch (payload.action) {
case 'call_state':
if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) {
if (payload.state === 'ended') {
powerSaveBlocker.stop(powerSaveBlockerId);
powerSaveBlockerId = null;
}
} else {
if (powerSaveBlockerId === null && payload.state === 'connected') {
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
}
}
break;
}
});
ipcMain.on('ipcCall', async function(ev, payload) {
if (!mainWindow) return;
const args = payload.args || [];
let ret;
switch (payload.name) {
case 'getUpdateFeedUrl':
ret = autoUpdater.getFeedURL();
break;
case 'getAutoLaunchEnabled':
ret = await launcher.isEnabled();
break;
case 'setAutoLaunchEnabled':
if (args[0]) {
launcher.enable();
} else {
launcher.disable();
}
break;
case 'setLanguage':
appLocalization.setAppLocale(args[0]);
break;
case 'shouldWarnBeforeExit':
ret = store.get('warnBeforeExit', true);
break;
case 'setWarnBeforeExit':
store.set('warnBeforeExit', args[0]);
break;
case 'getMinimizeToTrayEnabled':
ret = tray.hasTray();
break;
case 'setMinimizeToTrayEnabled':
if (args[0]) {
// Create trayIcon icon
tray.create(trayConfig);
} else {
tray.destroy();
}
store.set('minimizeToTray', args[0]);
break;
case 'getAutoHideMenuBarEnabled':
ret = global.mainWindow.autoHideMenuBar;
break;
case 'setAutoHideMenuBarEnabled':
store.set('autoHideMenuBar', args[0]);
global.mainWindow.autoHideMenuBar = Boolean(args[0]);
global.mainWindow.setMenuBarVisibility(!args[0]);
break;
case 'getAppVersion':
ret = app.getVersion();
break;
case 'focusWindow':
if (mainWindow.isMinimized()) {
mainWindow.restore();
} else if (!mainWindow.isVisible()) {
mainWindow.show();
} else {
mainWindow.focus();
}
break;
case 'getConfig':
ret = vectorConfig;
break;
case 'navigateBack':
if (mainWindow.webContents.canGoBack()) {
mainWindow.webContents.goBack();
}
break;
case 'navigateForward':
if (mainWindow.webContents.canGoForward()) {
mainWindow.webContents.goForward();
}
break;
case 'setSpellCheckLanguages':
if (args[0] && args[0].length > 0) {
mainWindow.webContents.session.setSpellCheckerEnabled(true);
store.set("spellCheckerEnabled", true);
try {
mainWindow.webContents.session.setSpellCheckerLanguages(args[0]);
} catch (er) {
console.log("There were problems setting the spellcheck languages", er);
}
} else {
mainWindow.webContents.session.setSpellCheckerEnabled(false);
store.set("spellCheckerEnabled", false);
}
break;
case 'getSpellCheckLanguages':
if (store.get("spellCheckerEnabled", true)) {
ret = mainWindow.webContents.session.getSpellCheckerLanguages();
} else {
ret = [];
}
break;
case 'getAvailableSpellCheckLanguages':
ret = mainWindow.webContents.session.availableSpellCheckerLanguages;
break;
case 'startSSOFlow':
recordSSOSession(args[0]);
break;
case 'getPickleKey':
try {
ret = await keytar.getPassword("element.io", `${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 (e) {
// 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
ret = null;
}
break;
case 'createPickleKey':
try {
const pickleKey = await randomArray(32);
await keytar.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey);
ret = pickleKey;
} catch (e) {
ret = null;
}
break;
case 'destroyPickleKey':
try {
await keytar.deletePassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`);
} catch (e) {}
break;
default:
mainWindow.webContents.send('ipcReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
mainWindow.webContents.send('ipcReply', {
id: payload.id,
reply: ret,
});
});
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
async function getOrCreatePassphrase(key) {
if (keytar) {
try {
const storedPassphrase = await keytar.getPassword("element.io", key);
if (storedPassphrase !== null) {
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);
}
} else {
return seshatDefaultPassphrase;
}
}
ipcMain.on('seshat', async function(ev, payload) {
if (!mainWindow) return;
const sendError = (id, e) => {
const error = {
message: e.message,
};
mainWindow.webContents.send('seshatReply', {
id: id,
error: error,
});
};
const args = payload.args || [];
let ret;
switch (payload.name) {
case 'supportsEventIndexing':
ret = seshatSupported;
break;
case 'initEventIndex':
if (eventIndex === null) {
const userId = args[0];
const deviceId = args[1];
const passphraseKey = `seshat|${userId}|${deviceId}`;
const passphrase = await getOrCreatePassphrase(passphraseKey);
try {
await afs.mkdir(eventStorePath, { recursive: true });
eventIndex = new Seshat(eventStorePath, { passphrase });
} catch (e) {
if (e instanceof ReindexError) {
// If this is a reindex error, the index schema
// changed. Try to open the database in recovery mode,
// reindex the database and finally try to open the
// database again.
const recoveryIndex = new SeshatRecovery(eventStorePath, {
passphrase,
});
const userVersion = await recoveryIndex.getUserVersion();
// If our user version is 0 we'll delete the db
// anyways so reindexing it is a waste of time.
if (userVersion === 0) {
await recoveryIndex.shutdown();
try {
await deleteContents(eventStorePath);
} catch (e) {
}
} else {
await recoveryIndex.reindex();
}
eventIndex = new Seshat(eventStorePath, { passphrase });
} else {
sendError(payload.id, e);
return;
}
}
}
break;
case 'closeEventIndex':
if (eventIndex !== null) {
const index = eventIndex;
eventIndex = null;
try {
await index.shutdown();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'deleteEventIndex':
{
try {
await deleteContents(eventStorePath);
} catch (e) {
}
}
break;
case 'isEventIndexEmpty':
if (eventIndex === null) ret = true;
else ret = await eventIndex.isEmpty();
break;
case 'isRoomIndexed':
if (eventIndex === null) ret = false;
else ret = await eventIndex.isRoomIndexed(args[0]);
break;
case 'addEventToIndex':
try {
eventIndex.addEvent(args[0], args[1]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'deleteEvent':
try {
ret = await eventIndex.deleteEvent(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'commitLiveEvents':
try {
ret = await eventIndex.commit();
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'searchEventIndex':
try {
ret = await eventIndex.search(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'addHistoricEvents':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addHistoricEvents(
args[0], args[1], args[2]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getStats':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getStats();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'removeCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.removeCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'addCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadFileEvents':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadFileEvents(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadCheckpoints':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadCheckpoints();
} catch (e) {
ret = [];
}
}
break;
case 'setUserVersion':
if (eventIndex === null) break;
else {
try {
await eventIndex.setUserVersion(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getUserVersion':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getUserVersion();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
default:
mainWindow.webContents.send('seshatReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
mainWindow.webContents.send('seshatReply', {
id: payload.id,
reply: ret,
});
});
app.commandLine.appendSwitch('--enable-usermedia-screen-capturing');
if (!app.commandLine.hasSwitch('enable-features')) {
app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer');
@@ -313,12 +826,6 @@ 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
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
// Disable hardware acceleration if the setting has been set.
if (global.store.get('disableHardwareAcceleration', false) === true) {
console.log("Disabling hardware acceleration.");
app.disableHardwareAcceleration();
}
app.on('ready', async () => {
try {
await setupGlobals();
@@ -376,7 +883,7 @@ app.on('ready', async () => {
target[target.length - 1] = 'index.html';
}
let baseDir: string;
let baseDir;
if (target[1] === 'webapp') {
baseDir = asarPath;
} else {
@@ -402,9 +909,9 @@ app.on('ready', async () => {
if (argv['no-update']) {
console.log('Auto update disabled via command line flag "--no-update"');
} else if (global.vectorConfig['update_base_url']) {
console.log(`Starting auto update with base URL: ${global.vectorConfig['update_base_url']}`);
updater.start(global.vectorConfig['update_base_url']);
} else if (vectorConfig['update_base_url']) {
console.log(`Starting auto update with base URL: ${vectorConfig['update_base_url']}`);
updater.start(vectorConfig['update_base_url']);
} else {
console.log('No update_base_url is defined: auto update is disabled');
}
@@ -416,13 +923,13 @@ app.on('ready', async () => {
});
const preloadScript = path.normalize(`${__dirname}/preload.js`);
global.mainWindow = new BrowserWindow({
mainWindow = global.mainWindow = new BrowserWindow({
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
backgroundColor: '#fff',
icon: iconPath,
show: false,
autoHideMenuBar: global.store.get('autoHideMenuBar', true),
autoHideMenuBar: store.get('autoHideMenuBar', true),
x: mainWindowState.x,
y: mainWindowState.y,
@@ -433,35 +940,35 @@ app.on('ready', async () => {
nodeIntegration: false,
//sandbox: true, // We enable sandboxing from app.enableSandbox() above
contextIsolation: true,
webgl: true,
webgl: false,
},
});
global.mainWindow.loadURL('vector://vector/webapp/');
mainWindow.loadURL('vector://vector/webapp/');
// Handle spellchecker
// 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));
// For some reason spellCheckerEnabled isn't persisted so we have to use the store here
mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true));
// Create trayIcon icon
if (global.store.get('minimizeToTray', true)) tray.create(global.trayConfig);
if (store.get('minimizeToTray', true)) tray.create(trayConfig);
global.mainWindow.once('ready-to-show', () => {
mainWindowState.manage(global.mainWindow);
mainWindow.once('ready-to-show', () => {
mainWindowState.manage(mainWindow);
if (!argv['hidden']) {
global.mainWindow.show();
mainWindow.show();
} else {
// hide here explicitly because window manage above sometimes shows it
global.mainWindow.hide();
mainWindow.hide();
}
});
global.mainWindow.webContents.on('before-input-event', warnBeforeExit);
mainWindow.webContents.on('before-input-event', warnBeforeExit);
global.mainWindow.on('closed', () => {
global.mainWindow = null;
mainWindow.on('closed', () => {
mainWindow = global.mainWindow = null;
});
global.mainWindow.on('close', async (e) => {
mainWindow.on('close', async (e) => {
// If we are not quitting and have a tray icon then minimize to tray
if (!global.appQuitting && (tray.hasTray() || process.platform === 'darwin')) {
// On Mac, closing the window just hides it
@@ -469,12 +976,12 @@ app.on('ready', async () => {
// behave, eg. Mail.app)
e.preventDefault();
if (global.mainWindow.isFullScreen()) {
global.mainWindow.once('leave-full-screen', () => global.mainWindow.hide());
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => mainWindow.hide());
global.mainWindow.setFullScreen(false);
mainWindow.setFullScreen(false);
} else {
global.mainWindow.hide();
mainWindow.hide();
}
return false;
@@ -483,19 +990,19 @@ app.on('ready', async () => {
if (process.platform === 'win32') {
// Handle forward/backward mouse buttons in Windows
global.mainWindow.on('app-command', (e, cmd) => {
if (cmd === 'browser-backward' && global.mainWindow.webContents.canGoBack()) {
global.mainWindow.webContents.goBack();
} else if (cmd === 'browser-forward' && global.mainWindow.webContents.canGoForward()) {
global.mainWindow.webContents.goForward();
mainWindow.on('app-command', (e, cmd) => {
if (cmd === 'browser-backward' && mainWindow.webContents.canGoBack()) {
mainWindow.webContents.goBack();
} else if (cmd === 'browser-forward' && mainWindow.webContents.canGoForward()) {
mainWindow.webContents.goForward();
}
});
}
webContentsHandler(global.mainWindow.webContents);
webContentsHandler(mainWindow.webContents);
global.appLocalization = new AppLocalization({
store: global.store,
appLocalization = new AppLocalization({
store,
components: [
() => tray.initApplicationMenu(),
() => Menu.setApplicationMenu(buildMenuTemplate()),
@@ -508,12 +1015,14 @@ app.on('window-all-closed', () => {
});
app.on('activate', () => {
global.mainWindow.show();
mainWindow.show();
});
function beforeQuit(): void {
function beforeQuit() {
global.appQuitting = true;
global.mainWindow?.webContents.send('before-quit');
if (mainWindow) {
mainWindow.webContents.send('before-quit');
}
}
app.on('before-quit', beforeQuit);
@@ -524,10 +1033,10 @@ app.on('second-instance', (ev, commandLine, workingDirectory) => {
if (commandLine.includes('--hidden')) return;
// Someone tried to run a second instance, we should focus our window.
if (global.mainWindow) {
if (!global.mainWindow.isVisible()) global.mainWindow.show();
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
global.mainWindow.focus();
if (mainWindow) {
if (!mainWindow.isVisible()) mainWindow.show();
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});

View File

@@ -1,47 +0,0 @@
{
"Add to dictionary": "Добави към речника",
"The image failed to save": "Изображението не успя да се запази",
"Failed to save image": "Неуспешно запазване на изображението",
"Save image as...": "Запази изображението като...",
"Copy link address": "Копирай линка",
"Copy image address": "Копирай адреса на изображението",
"Copy email address": "Копирай имейл адрес",
"Copy image": "Копирай изображение",
"File": "Файл",
"Bring All to Front": "Покажи всички най-отгоре",
"Zoom": "Мащабирай",
"Stop Speaking": "Спри да говориш",
"Start Speaking": "Започни да говориш",
"Speech": "Говор",
"Unhide": "Покажи",
"Hide Others": "Скрий Останалите",
"Hide": "Скрий",
"Services": "Услуги",
"About": "Относно",
"Element Help": "Помощ за Елемент",
"Help": "Помощ",
"Close": "Затвори",
"Minimize": "Минимизирай",
"Window": "Прозорец",
"Toggle Developer Tools": "Превключи инструментите за разработчици",
"Toggle Full Screen": "Превключи на Цял екран",
"Preferences": "Предпочитания",
"Zoom Out": "Намали",
"Zoom In": "Увеличи",
"Actual Size": "Действителен Размер",
"View": "Преглед",
"Select All": "Избери Всичко",
"Delete": "Изтрий",
"Paste and Match Style": "Постави и Използвай текущия стил",
"Paste": "Постави",
"Copy": "Копирай",
"Cut": "Изрежи",
"Redo": "Върни",
"Undo": "Отмени",
"Edit": "Редактирай",
"Quit": "Напусни",
"Show/Hide": "Покажи/Скрий",
"Are you sure you want to quit?": "Сигурен ли си че искаш да напуснеш?",
"Close Element": "Затвори Елемент",
"Cancel": "Отказ"
}

View File

@@ -1,47 +0,0 @@
{
"Are you sure you want to quit?": "তুমি কি আসলেই বের হতে চাও?",
"Close Element": "এলিমেন্ট বন্ধ করো",
"Cancel": "বাতিল",
"Save image as...": "ছবি সংরক্ষণের ধরন...",
"Failed to save image": "ছবি সংরক্ষণ ব্যর্থ",
"The image failed to save": "ছবি সংরক্ষণ ব্যর্থ",
"Add to dictionary": "অভিধানে যোগ করি",
"Copy link address": "সংযোগের ঠিকানা অনুলিপি করো",
"Copy image address": "ছবির ঠিকানা অনুলিপি করো",
"Copy email address": "ইমেইল ঠিকানা অনুলিপি করো",
"Copy image": "ছবি অনুলিপি করো",
"File": "নথি",
"Bring All to Front": "সবকিছু সামনে আনো",
"Zoom": "বড় করা",
"Stop Speaking": "কথা বন্ধ করো",
"Start Speaking": "কথা শুরু করো",
"Speech": "বাচন",
"Unhide": "দেখাও",
"Hide Others": "অন্যগুলো লুকাও",
"Hide": "লুকাও",
"Services": "সেবা",
"About": "আমাদের সম্পর্কে",
"Element Help": "এলিমেন্ট সাহায্য",
"Help": "সাহায্য",
"Close": "বন্ধ",
"Minimize": "সংকোচন",
"Window": "জানালা",
"Toggle Developer Tools": "ডেভেলপার সরঞ্জামাদি",
"Toggle Full Screen": "পূর্ণ পর্দা করো/বের হও",
"Preferences": "পছন্দসমূহ",
"Zoom Out": "ছোট করো",
"Zoom In": "বড়ো করো",
"Actual Size": "আসল আকার",
"View": "দেখো",
"Select All": "সব নির্বাচন",
"Delete": "অপসারণ",
"Paste and Match Style": "লেপন ও একই ধরনে",
"Paste": "লেপন",
"Copy": "অনুলিপি",
"Cut": "কাটো",
"Redo": "পুন",
"Undo": "ফিরত",
"Edit": "সম্পাদনা",
"Quit": "প্রস্থান",
"Show/Hide": "দেখাও/লুকাও"
}

View File

@@ -42,7 +42,5 @@
"Quit": "Beenden",
"Show/Hide": "Anzeigen/Ausblenden",
"Close Element": "Element schließen",
"Cancel": "Abbrechen",
"Copy image address": "Bild-Adresse kopieren",
"Close %(brand)s": "%(brand)s schließen"
"Cancel": "Abbrechen"
}

View File

@@ -1,47 +0,0 @@
{
"Are you sure you want to quit?": "Είστε βέβαιος ότι θέλετε να εγκαταλείψετε;",
"Zoom": "Ζουμ",
"Unhide": "Εμφάνιση",
"Window": "Παράθυρο",
"Toggle Developer Tools": "Άνοιγμα Εργαλείων Προγραμματιστή",
"Toggle Full Screen": "Εναλλαγή σε Πλήρη Οθόνη",
"Copy email address": "Αντιγραφή διεύθυνσης email",
"File": "Αρχείο",
"Bring All to Front": "Μεταφορά Όλων στο Προσκήνιο",
"Stop Speaking": "Τερματίστε να μιλάτε",
"Start Speaking": "Ξεκινήστε να μιλάτε",
"Speech": "Ομιλία",
"Hide Others": "Απόκρυψη Άλλων",
"Hide": "Απόκρυψη",
"Services": "Υπηρεσίες",
"About": "Σχετικά με",
"Element Help": "Βοήθεια για το Element",
"Help": "Βοήθεια",
"Close": "Κλείσιμο",
"Minimize": "Ελαχιστοποίηση",
"Preferences": "Προτιμήσεις",
"Zoom Out": "Σμίκρυνση",
"Zoom In": "Μεγέθυνση",
"Actual Size": "Πραγματικό Μέγεθος",
"View": "Προβολή",
"Select All": "Επιλογή Όλων",
"Delete": "Διαγραφή",
"Paste and Match Style": "Επικόλληση και Ταίριασμα Στυλ",
"Paste": "Επικόλληση",
"Copy": "Αντιγραφή",
"Cut": "Αποκοπή",
"Redo": "Επανάληψη",
"Undo": "Αναίρεση",
"Edit": "Επεξεργασία",
"Quit": "Κλείσιμο",
"Show/Hide": "Eμφάνιση/Απόκρυψη",
"Close Element": "Κλείστε το Element",
"Cancel": "Ακύρωση",
"Add to dictionary": "Προσθήκη στο λεξικό",
"The image failed to save": "Η αποθήκευση της εικόνας απέτυχε",
"Failed to save image": "Αποτυχία αποθήκευσης εικόνας",
"Save image as...": "Αποθήκευση εικόνας ως...",
"Copy link address": "Αντιγραφή διεύθυνσης συνδέσμου",
"Copy image address": "Αντιγραφή διεύθυνσης εικόνας",
"Copy image": "Αντιγραφή εικόνας"
}

View File

@@ -1,6 +1,6 @@
{
"Cancel": "Cancel",
"Close %(brand)s": "Close %(brand)s",
"Close Element": "Close Element",
"Are you sure you want to quit?": "Are you sure you want to quit?",
"Show/Hide": "Show/Hide",
"Quit": "Quit",
@@ -38,7 +38,6 @@
"File": "File",
"Copy image": "Copy image",
"Copy email address": "Copy email address",
"Copy image address": "Copy image address",
"Copy link address": "Copy link address",
"Save image as...": "Save image as...",
"Failed to save image": "Failed to save image",

View File

@@ -1,21 +0,0 @@
{
"The image failed to save": "La bildo malsukcesis elŝutiĝi",
"Failed to save image": "Malsukcesis elŝuti bildon",
"Stop Speaking": "Ĉesi Paroli",
"Start Speaking": "Ekparoli",
"Unhide": "Malkaŝi",
"Hide Others": "Kaŝi Aliajn",
"Hide": "Kaŝi",
"About": "Informilo",
"Element Help": "Element-a Helpo",
"Help": "Helpo",
"Toggle Developer Tools": "Baskuligi Programistajn Ilojn",
"Preferences": "Preferoj",
"View": "Vidi",
"Delete": "Forigi",
"Redo": "Refari",
"Undo": "Malfari",
"Edit": "Redakti",
"Show/Hide": "Montri/Kaŝi",
"Cancel": "Nuligi"
}

View File

@@ -42,7 +42,5 @@
"Show/Hide": "Ver/Ocultar",
"Are you sure you want to quit?": "¿Quieres salir?",
"Close Element": "Cerrar Element",
"Cancel": "Cancelar",
"Copy image address": "Copiar dirección de la imagen",
"Close %(brand)s": "Cerrar %(brand)s"
"Cancel": "Cancelar"
}

View File

@@ -42,7 +42,5 @@
"Show/Hide": "Näita/peida",
"Are you sure you want to quit?": "Kas sa kindlasti soovid rakendusest väljuda?",
"Close Element": "Sulge Element",
"Cancel": "Tühista",
"Copy image address": "Kopeeri pildi aadress",
"Close %(brand)s": "Sulge %(brand)s"
"Cancel": "Tühista"
}

View File

@@ -1,48 +0,0 @@
{
"Paste and Match Style": "جای‌گذاری و تطبیق سَبک",
"Add to dictionary": "افزودن به لغت‌نامه",
"The image failed to save": "تصویر ذخیره نشد",
"Failed to save image": "ذخیرهٔ تصویر شکست خورد",
"Save image as...": "ذخیرهٔ تصویر به عنوان...",
"Copy link address": "رونوشت نشانی پیوند",
"Copy image address": "رونوشت نشانی تصویر",
"Copy email address": "رونوشت نشانی رایانامه",
"Copy image": "رونوشت تصویر",
"File": "پرونده",
"Bring All to Front": "همه را به جلو بیاورید",
"Zoom": "بزرگنمایی",
"Speech": "صحبت کردن",
"Stop Speaking": "صحبت کردن را تمام کنید",
"Start Speaking": "صحبت کردن را شروع کنید",
"Unhide": "آشکار",
"Hide Others": "پنهان کردن دیگران",
"Hide": "پنهان",
"Services": "خدمات",
"About": "درباره",
"Element Help": "راهنمای المنت",
"Help": "راهنما",
"Close": "بستن",
"Minimize": "کمینه",
"Window": "پنجره",
"Toggle Developer Tools": "تغییر وضعیت ابزارهای توسعه‌دهنده",
"Toggle Full Screen": "تغییر وضعیت تمام‌صفحه",
"Preferences": "ترجیحات",
"Zoom Out": "بزرگنمایی به خارج",
"Zoom In": "بزرگنمایی به داخل",
"Actual Size": "اندازهٔ واقعی",
"View": "دیدن",
"Select All": "گزینش همه",
"Delete": "حذف",
"Paste": "جای‌گذاری",
"Copy": "رونوشت",
"Cut": "برش",
"Redo": "انجام دوباره",
"Undo": "بازگردانی",
"Edit": "ویرایش",
"Quit": "خروج",
"Show/Hide": "نمایش/پنهان",
"Are you sure you want to quit?": "آیا مطمئنید که می‌خواهید خارج شوید؟",
"Close Element": "بستن المنت",
"Cancel": "لغو",
"Close %(brand)s": "بستن %(brand)s"
}

View File

@@ -42,7 +42,5 @@
"Show/Hide": "Näytä/piilota",
"Are you sure you want to quit?": "Haluatko varmasti poistua?",
"Close Element": "Sulje Element",
"Cancel": "Peruuta",
"Copy image address": "Kopioi kuvan osoite",
"Close %(brand)s": "Sulje %(brand)s"
"Cancel": "Peruuta"
}

View File

@@ -41,7 +41,5 @@
"Bring All to Front": "Tout amener au premier plan",
"Zoom": "Zoom",
"Stop Speaking": "Arrêter la dictée",
"Start Speaking": "Commencer la dictée",
"Copy image address": "Copier l'adresse de l'image",
"Redo": "Refaire"
"Start Speaking": "Commencer la dictée"
}

View File

@@ -41,7 +41,5 @@
"Show/Hide": "הצג\\הסתר",
"Are you sure you want to quit?": "האם אתה בטוח שברצונך לצאת?",
"Close Element": "סגור את אלמנט",
"Cancel": "ביטול",
"Paste and Match Style": "הדבק והתאם סגנון",
"Copy image address": "העתקת כתובת התמונה"
"Cancel": "ביטול"
}

View File

@@ -29,7 +29,7 @@
"Zoom In": "Nagyít",
"Actual Size": "Jelenlegi méret",
"View": "Nézet",
"Select All": "Összes kijelölése",
"Select All": "Mind kijelölése",
"Delete": "Töröl",
"Paste and Match Style": "Beillesztés formázással",
"Paste": "Beillesztés",
@@ -42,7 +42,5 @@
"Show/Hide": "Megmutat/Elrejt",
"Are you sure you want to quit?": "Biztos, hogy kilép?",
"Close Element": "Element bezárása",
"Cancel": "Mégsem",
"Copy image address": "Kép címének másolása",
"Close %(brand)s": "%(brand)s bezárása"
"Cancel": "Mégsem"
}

View File

@@ -6,7 +6,7 @@
"Copy link address": "Salin alamat tautan",
"Copy email address": "Salin surel",
"Copy image": "Salin gambar",
"File": "File",
"File": "Berkas",
"Hide Others": "Sembunyikan yang Lain",
"Bring All to Front": "Bawa Semua ke Depan",
"Zoom": "Perbesar",
@@ -37,12 +37,10 @@
"Paste and Match Style": "Tempel dan Cocokkan Gaya",
"Paste": "Tempel",
"Copy": "Salin",
"Edit": "Edit",
"Edit": "Sunting",
"Quit": "Keluar",
"Show/Hide": "Tampilkan/Sembunyikan",
"Are you sure you want to quit?": "Apakah Anda yakin ingin keluar?",
"Close Element": "Tutup Element",
"Cancel": "Batal",
"Copy image address": "Salin alamat gambar",
"Close %(brand)s": "Tutuo %(brand)s"
"Cancel": "Batal"
}

View File

@@ -1,37 +1,37 @@
{
"Add to dictionary": "Bæta við orðasafn",
"The image failed to save": "Myndina var ekki hægt að vista",
"Add to dictionary": "Bæta við orðabók",
"The image failed to save": "Myndin mistóksts að vista",
"Failed to save image": "Mistókst að vista mynd",
"Save image as...": "Vista mynd sem...",
"Copy link address": "Afrita vistfang tengils",
"Copy link address": "Afrita nethlekk",
"Copy email address": "Afrita tölvupóstfang",
"Copy image": "Afrita mynd",
"File": "Skrá",
"Bring All to Front": "Setja allt fremst",
"Bring All to Front": "Koma Öllum að Framan",
"Zoom": "Stærð",
"Stop Speaking": "Hætta tali",
"Start Speaking": "Byrja tal",
"Stop Speaking": "Hætta Tal",
"Start Speaking": "Byrja Tal",
"Speech": "Tal",
"Unhide": "Birta",
"Hide Others": "Fela aðra",
"Hide Others": "Fela Aðra",
"Hide": "Fela",
"Services": "Þjónustur",
"About": "Um hugbúnaðinn",
"Element Help": "Hjálp við Element",
"About": "Um",
"Element Help": "Element Hjálp",
"Help": "Hjálp",
"Close": "Loka",
"Minimize": "Lágmarka",
"Window": "Gluggi",
"Toggle Developer Tools": "Víxla forritunarverkfærum af/á",
"Toggle Full Screen": "Víxla fullum skjá af/á",
"Toggle Developer Tools": "Skipta Framkvæmdaraðilaverkfæri",
"Toggle Full Screen": "Skipta um Fullskjá",
"Preferences": "Stillingar",
"Zoom Out": "Minnka",
"Zoom In": "Stækka",
"Actual Size": "Raunstærð",
"Actual Size": "Raunveruleg Stærð",
"View": "Skoða",
"Select All": "Velja allt",
"Select All": "Velja Allt",
"Delete": "Eyða",
"Paste and Match Style": "Líma og samsvara stíl",
"Paste and Match Style": "Líma og Passa Stíl",
"Paste": "Líma",
"Copy": "Afrita",
"Cut": "Klippa",
@@ -42,6 +42,5 @@
"Show/Hide": "Sýna/Fela",
"Are you sure you want to quit?": "Ertu viss um að þú viljir hætta?",
"Close Element": "Loka Element",
"Cancel": "Hætta við",
"Copy image address": "Afrita slóð myndar"
"Cancel": "Hætta við"
}

View File

@@ -42,7 +42,5 @@
"Close Element": "Chiudi Element",
"Cancel": "Annulla",
"Stop Speaking": "Smetti di parlare",
"Speech": "Dettatura",
"Copy image address": "Copia indirizzo immagine",
"Close %(brand)s": "Chiudi %(brand)s"
"Speech": "Dettatura"
}

View File

@@ -1,47 +0,0 @@
{
"Toggle Developer Tools": "ສະຫຼັບໄປໜ້າເຄື່ອງມືພັດທະນາ",
"Add to dictionary": "ເພີ່ມເຂົ້າໄປວັດຈະນານຸກົມ",
"The image failed to save": "ຮູບພາບບໍ່ສາມາດບັດທຶກໄດ້",
"Failed to save image": "ການບັນທຶກຮູບພາບບໍ່ສຳເລັດ",
"Save image as...": "ບັນທຶກຮູບພາບເປັນ...",
"Copy link address": "ສຳເນົາທີ່ຢູ່ລິ້ງ",
"Copy image address": "ສຳເນົາທີ່ຢູ່ຮູບພາບ",
"Copy email address": "ສຳເນົາທີ່ຢູ່ເມວ",
"Copy image": "ສຳເນົາຮູບ",
"File": "ຟາຍ",
"Bring All to Front": "ເອົາທັງໝົດມາທາງໜ້າ",
"Zoom": "ຊູມ",
"Stop Speaking": "ເຊົາສົນທະນາ",
"Start Speaking": "ເລີ່ມສົນທະນາ",
"Speech": "ຄຳກ່າວ",
"Unhide": "ໂຊຄືນ",
"Hide Others": "ເຊື່ອງອັນອື່ນ",
"Hide": "ເຊື່ອງ",
"Services": "ບໍລິການ",
"About": "ກ່ຽວກັບ",
"Element Help": "ລະບົບຊ່ວຍເຫຼືອ",
"Help": "ຊ່ວຍເຫຼືອ",
"Close": "ປິດ",
"Minimize": "ຫຍໍ້ນ້ອຍ",
"Window": "ປ່ອງຢ້ຽມ",
"Toggle Full Screen": "ສະຫຼັບເຕັມຈໍ",
"Preferences": "ການຕັ້ງຄ່າ",
"Zoom Out": "ຊູມອອກ",
"Zoom In": "ຊູມເຂົ້າ",
"Actual Size": "ຂະໜາດຕົວຈິງ",
"View": "ເບິ່ງ",
"Select All": "ເລືອກທັງໝົດ",
"Delete": "ລຶບ",
"Paste and Match Style": "ກັອບມາໃສ່ ແລະໃຫ້ສະຕາຍຕົງກັນ",
"Paste": "ກັອບມາໃສ່",
"Copy": "ສຳເນົາ",
"Cut": "ຕັດ",
"Redo": "ລຶ້ມຄືນ",
"Undo": "ຮື້ຄືນ",
"Edit": "ແກ້ໄຂ",
"Quit": "ຍົກເລີກ",
"Show/Hide": "ສະແດງ/ເຊື່ອງ",
"Are you sure you want to quit?": "ທ່ານຕ້ອງການປິດແທ້ບໍ່?",
"Close Element": "ປິດລະບົບ",
"Cancel": "ຍົກເລີກ"
}

View File

@@ -1,47 +0,0 @@
{
"Failed to save image": "Nepavyko įrašyti paveikslėlio",
"Save image as...": "Įrašyti paveikslėlį kaip...",
"Copy image address": "Kopijuoti paveikslėlio adresą",
"Copy image": "Kopijuoti paveikslėlį",
"The image failed to save": "Paveikslėlio nepavyko išsaugoti",
"Bring All to Front": "Viską iškelti į priekį",
"Speech": "Kalba",
"Actual Size": "Tikrasis dydis",
"Toggle Developer Tools": "Perjungti kūrėjo įrankius",
"Toggle Full Screen": "Perjungti viso ekrano režimą",
"Paste and Match Style": "Įklijuoti ir suderinti stilių",
"Redo": "Sugrąžinti veiksmą",
"Undo": "Atšaukti veiksmą",
"Select All": "Pasirinkti visus",
"Delete": "Ištrinti",
"Paste": "Įklijuoti",
"Copy": "Kopijuoti",
"Cut": "Iškirpti",
"Add to dictionary": "Pridėti prie žodyno",
"Copy link address": "Kopijuoti nuorodos adresą",
"Copy email address": "Kopijuoti el. pašto adresą",
"File": "Failas",
"Zoom": "Priartinti",
"Stop Speaking": "Nustoti kalbėti",
"Start Speaking": "Pradėti kalbėti",
"Unhide": "Nebeslėpti",
"Hide Others": "Slėpti kitus",
"Hide": "Slėpti",
"Services": "Paslaugos",
"About": "Apie",
"Element Help": "Element Pagalba",
"Help": "Pagalba",
"Close": "Uždaryti",
"Minimize": "Sumažinti",
"Window": "Langas",
"Preferences": "Nuostatos",
"Zoom Out": "Atitolinti",
"Zoom In": "Priartinti",
"View": "Peržiūrėti",
"Edit": "Redaguoti",
"Quit": "Išeiti",
"Show/Hide": "Rodyti/Slėpti",
"Are you sure you want to quit?": "Ar tikrai norite išeiti?",
"Close Element": "Uždaryti Element",
"Cancel": "Atšaukti"
}

View File

@@ -1,11 +1,11 @@
{
"Start Speaking": "Runājiet...",
"Add to dictionary": "Pievienot vārdnīcai",
"The image failed to save": "Attēlu neizdevās saglabāt",
"The image failed to save": "Attēlu saglabāt neizdevās",
"Failed to save image": "Neizdevās saglabāt attēlu",
"Save image as...": "Saglabāt attēlu kā...",
"Copy link address": "Kopēt saiti",
"Copy email address": "Kopēt e-pasta adresi",
"Copy email address": "Kopēt e-pastu",
"Copy image": "Kopēt attēlu",
"File": "Fails",
"Bring All to Front": "Iznest visu priekšplānā",
@@ -15,7 +15,7 @@
"Unhide": "Rādīt",
"Hide Others": "Slēpt citus",
"Hide": "Slēpt",
"Services": "Pakalpojumi",
"Services": "Servisi/pakalpojumi",
"About": "Par programmu",
"Element Help": "Element palīdzība",
"Help": "Palīdzība",
@@ -24,7 +24,7 @@
"Window": "Logs",
"Toggle Developer Tools": "Pārslēgt uz Izstrādātāja rīkiem",
"Toggle Full Screen": "Pārslēgt uz pilnekrānu",
"Preferences": "Iestatījumi",
"Preferences": "Parametri/iestatījumi",
"Zoom Out": "Samazināt",
"Zoom In": "Palielināt",
"Actual Size": "Faktiskais izmērs",
@@ -35,14 +35,12 @@
"Paste": "Ievietot",
"Copy": "Kopēt",
"Cut": "Izgriezt",
"Redo": "Atatgriezt/atatsaukt/atatdarīt",
"Undo": "Atgriezt/atsaukt/atdarīt",
"Redo": "Atatdarīt/atatgriezt (redo)",
"Undo": "Atgreizt/atdarīt (undo)",
"Edit": "Rediģēt",
"Quit": "Iziet",
"Show/Hide": "Rādīt/nerādīt",
"Are you sure you want to quit?": "Tiešām vēlaties iziet?",
"Close Element": "Aizvērt Elementu",
"Cancel": "Atcelt",
"Copy image address": "Kopēt attēla adresi",
"Close %(brand)s": "Aizvērt %(brand)s"
"Cancel": "Atcelt"
}

View File

@@ -13,11 +13,11 @@
"Start Speaking": "Begin met praten",
"Speech": "Spraak",
"Unhide": "Weer laten zien",
"Hide Others": "Anderen verbergen",
"Hide Others": "Andere verbergen",
"Hide": "Verbergen",
"Services": "Diensten",
"About": "Over",
"Element Help": "Element-hulp",
"Element Help": "Element Help",
"Help": "Help",
"Close": "Sluiten",
"Minimize": "Minimaliseren",
@@ -42,7 +42,5 @@
"Show/Hide": "Tonen/Verbergen",
"Are you sure you want to quit?": "Weet u zeker dat u wilt stoppen?",
"Close Element": "Element sluiten",
"Cancel": "Annuleren",
"Copy image address": "Kopieer afbeeldingsadres",
"Close %(brand)s": "Sluit %(brand)s"
"Cancel": "Annuleren"
}

View File

@@ -1,46 +0,0 @@
{
"The image failed to save": "Biletet vart ikkje lagra",
"Paste and Match Style": "Lim inn og tilpass stil",
"Redo": "Gjer om",
"Undo": "Angre",
"Are you sure you want to quit?": "Er du sikker på at du vil avslutta?",
"Add to dictionary": "Legg til i ordlista",
"Failed to save image": "Klarte ikkje å lagra biletet",
"Save image as...": "Lagre bilete som…",
"Copy link address": "Kopier lenkjeadresse",
"Copy email address": "Kopier e-postadresse",
"Copy image": "Kopier bilete",
"File": "Fil",
"Bring All to Front": "Plasser lengst fram",
"Zoom": "Zoom",
"Stop Speaking": "Stopp snakka",
"Start Speaking": "Byrja snakka",
"Speech": "Tale",
"Unhide": "Ikkje gøym",
"Hide Others": "Gøym andre",
"Hide": "Gøym",
"Services": "Tenester",
"About": "Om",
"Element Help": "Hjelp med Element",
"Help": "Hjelp",
"Close": "Lat att",
"Minimize": "Minimer",
"Window": "Vindauga",
"Toggle Developer Tools": "Developer Tools av/på",
"Toggle Full Screen": "Fullskjerm av/på",
"Preferences": "Innstillingar",
"Zoom Out": "Zoom ut",
"Zoom In": "Zoom inn",
"Actual Size": "Faktisk storleik",
"View": "Vis",
"Select All": "Marker alt",
"Delete": "Slett",
"Paste": "Lim inn",
"Copy": "Lim inn",
"Cut": "Klipp ut",
"Edit": "Rediger",
"Quit": "Avslutt",
"Show/Hide": "Vis/Gøym",
"Close Element": "Lat att Element",
"Cancel": "Avbryt"
}

View File

@@ -42,6 +42,5 @@
"Show/Hide": "Pokaż/Ukryj",
"Are you sure you want to quit?": "Czy na pewno chcesz zamknąć?",
"Close Element": "Zamknij Elementa",
"Cancel": "Anuluj",
"Copy image address": "Skopiuj adres obrazu"
"Cancel": "Anuluj"
}

View File

@@ -42,7 +42,5 @@
"Close Element": "Fechar Element",
"Cancel": "Cancelar",
"Bring All to Front": "Trazer Todas Para Frente",
"Hide Others": "Esconder Outras(os)",
"Copy image address": "Copiar endereço de imagem",
"Close %(brand)s": "Fechar %(brand)s"
"Hide Others": "Esconder Outras(os)"
}

View File

@@ -35,9 +35,9 @@
"Redo": "Refă",
"Undo": "Anulare",
"Edit": "Editare",
"Quit": "Închid",
"Quit": "Închide",
"Show/Hide": "Arată/Ascunde",
"Are you sure you want to quit?": "Sigur vrei să ieși din cont?",
"Close Element": "Închid Element",
"Are you sure you want to quit?": "Sunte-ți sigur ca doriți sa inchideți aplicația ?",
"Close Element": "închide aplicația",
"Cancel": "Anulare"
}

View File

@@ -42,7 +42,5 @@
"Start Speaking": "Говорите",
"Speech": "Голос",
"Hide Others": "Скрыть прочие",
"Paste and Match Style": "Вставить с тем же стилем",
"Copy image address": "Копировать адрес изображения",
"Close %(brand)s": "Закрыть %(brand)s"
"Paste and Match Style": "Вставить с тем же стилем"
}

View File

@@ -27,21 +27,5 @@
"Redo": "පසුසේ",
"Undo": "පෙරසේ",
"Edit": "සංස්කරණය",
"Quit": "ඉවත් වන්න",
"Paste and Match Style": "අලවා ශෛලිය ගැළපුම",
"Delete": "මකන්න",
"The image failed to save": "රූපය සුරැකීමට අසමත්",
"Failed to save image": "රූපය සුරැකීමට අසමත්",
"Save image as...": "...ලෙස රූපය සුරකින්න",
"Copy image address": "රූපයේ ලිපිනයේ පිටපතක්",
"Copy image": "රූපයෙහි පිටපතක්",
"Bring All to Front": "සියල්ල ඉදිරිපසට",
"Stop Speaking": "කථාව නිමාව",
"Start Speaking": "කථාව ආරම්භය",
"Speech": "කථාව",
"Unhide": "නොසඟවන්න",
"Toggle Developer Tools": "සංවර්ධක මෙවලම්",
"Toggle Full Screen": "පූර්ණ තිරයට",
"Preferences": "පෙනුම",
"View": "දකින්න"
"Quit": "ඉවත් වන්න"
}

View File

@@ -1,48 +0,0 @@
{
"Unhide": "Odkryť",
"Stop Speaking": "Zastaviť nahrávanie hlasu",
"Start Speaking": "Spustiť nahrávanie hlasu",
"Speech": "Reč",
"Element Help": "Pomocník pre aplikáciu Element",
"Paste and Match Style": "Vložiť a prispôsobiť štýl",
"Close Element": "Zavrieť aplikáciu Element",
"Add to dictionary": "Pridať do slovníka",
"The image failed to save": "Obrázok sa nepodarilo uložiť",
"Failed to save image": "Chyba pri ukladaní obrázka",
"Save image as...": "Uložiť obrázok ako...",
"Copy link address": "Kopírovať adresu odkazu",
"Copy email address": "Kopírovať e-mailovú adresu",
"Copy image": "Kopírovať obrázok",
"File": "Súbor",
"Bring All to Front": "Preniesť všetky do popredia",
"Zoom": "Lupa",
"Hide Others": "Skryť ostatné",
"Hide": "Skryť",
"Services": "Služby",
"About": "O aplikácii",
"Help": "Pomocník",
"Close": "Zavrieť",
"Minimize": "Minimalizovať",
"Window": "Okno",
"Toggle Developer Tools": "Nástroje pre vývojárov",
"Toggle Full Screen": "Celá obrazovka",
"Preferences": "Vlastnosti",
"Zoom Out": "Oddialiť",
"Zoom In": "Priblížiť",
"Actual Size": "Aktuálna veľkosť",
"View": "Zobraziť",
"Select All": "Vybrať všetko",
"Delete": "Odstrániť",
"Paste": "Vložiť",
"Copy": "Kopírovať",
"Cut": "Vystrihnúť",
"Redo": "Opakovať",
"Undo": "Späť",
"Edit": "Úpravy",
"Quit": "Ukončiť",
"Show/Hide": "Zobraziť/Skryť",
"Are you sure you want to quit?": "Naozaj chcete zavrieť aplikáciu?",
"Cancel": "Zrušiť",
"Copy image address": "Kopírovať adresu obrázka",
"Close %(brand)s": "Zatvoriť %(brand)s"
}

View File

@@ -1,5 +1,5 @@
{
"Save image as...": "Spara bild som",
"Save image as...": "Spara bild som...",
"Copy link address": "Kopiera länkadress",
"Copy email address": "Kopiera e-postadress",
"Copy image": "Kopiera bild",
@@ -12,7 +12,7 @@
"Hide": "Göm",
"Services": "Tjänster",
"About": "Om",
"Element Help": "Element-Hjälp",
"Element Help": "Element Hjälp",
"Help": "Hjälp",
"Close": "Stäng",
"Minimize": "Minimera",
@@ -34,7 +34,7 @@
"Zoom": "Zooma",
"Toggle Developer Tools": "Växla utvecklarverktyg",
"Toggle Full Screen": "Växla helskärm",
"Unhide": "Sluta gömma",
"Unhide": "Göm inte",
"Zoom Out": "Zooma ut",
"Zoom In": "Zooma in",
"Close Element": "Stäng Element",
@@ -42,7 +42,5 @@
"Add to dictionary": "Lägg till i ordlistan",
"The image failed to save": "Bilden sparades inte",
"Failed to save image": "Misslyckades med att spara bilden",
"Are you sure you want to quit?": "Är du säker att du vill avsluta?",
"Copy image address": "Kopiera bildadress",
"Close %(brand)s": "Stäng %(brand)s"
"Are you sure you want to quit?": "Är du säker att du vill avsluta?"
}

View File

@@ -1,9 +1,9 @@
{
"Zoom": "பெரிதாக்குதல்",
"Minimize": "சிறிதாக்கு",
"Toggle Developer Tools": "உருவாக்குநர் கருவிகளை நிலைமாற்று",
"Toggle Developer Tools": "படைப்பாளர் கருவிகளை நிலைமாற்று",
"Toggle Full Screen": "முழு திரையை நிலைமாற்று",
"Paste and Match Style": "ஒட்டு மற்றும் நடையை பொுத்து",
"Paste and Match Style": "ஒட்டு மற்றும் நடையை பொுத்து",
"Add to dictionary": "அகராதியில் சேர்",
"The image failed to save": "படம் சேமிக்கத் தவறிவிட்டது",
"Failed to save image": "படத்தைச் சேமிப்பதில் தோல்வி",
@@ -16,8 +16,8 @@
"Stop Speaking": "பேசுவதை நிறுத்து",
"Start Speaking": "பேசத் துவங்கு",
"Speech": "பேச்சு",
"Unhide": "மறைநீக்கு",
"Hide Others": "மற்றவற்றை மறை",
"Unhide": "காட்டு",
"Hide Others": "மற்றை மறை",
"Hide": "மறை",
"Services": "சேவைகள்",
"About": "இதனைப் பற்றி",
@@ -29,8 +29,8 @@
"Zoom Out": "சிறிதாக்கு",
"Zoom In": "பெரிதாக்கு",
"Actual Size": "உண்மையான அளவு",
"View": "காட்டு",
"Select All": "அனைத்தையும் தேர்ந்தெடு",
"View": "காட்சி",
"Select All": "அனைத்தையும் தெரிவுசெய்",
"Delete": "அழி",
"Paste": "ஒட்டு",
"Copy": "நகலெடு",
@@ -42,6 +42,5 @@
"Show/Hide": "காட்டு/மறை",
"Are you sure you want to quit?": "நீங்கள் நிச்சயம் வெளியேற விரும்புகிறீர்களா?",
"Close Element": "எலிமெண்ட் ஐ மூடு",
"Cancel": "விலக்கிக்கொள்",
"Copy image address": "பட முகவரியை நகலெடு"
"Cancel": "ரத்துசெய்"
}

View File

@@ -1,10 +0,0 @@
{
"About": "గ్రెంచ్",
"Paste and Match Style": "మునుపటి వంటి అతికించండి",
"Paste": "పేస్ట్",
"Cut": "కట్",
"Copy": "కాపీ",
"Are you sure you want to quit?": "మీరు వెళ్ళిపోవాలని అనుకుంటున్నారా?",
"Close Element": "మూసివేత element",
"Cancel": "ఆపు"
}

View File

@@ -1,47 +0,0 @@
{
"Add to dictionary": "Sözlüğe ekle",
"The image failed to save": "Bu resim kaydedilemedi",
"Failed to save image": "Resim kaydedilemedi",
"Save image as...": "Resmi ... olarak farklı kaydet",
"Copy link address": "Bağlantılı adresi kopyala",
"Copy email address": "E-posta adresini kopyala",
"Copy image": "Resmi kopyala",
"File": "Dosya",
"Bring All to Front": "Hepsini öne getir",
"Zoom": "Yaklaştır",
"Stop Speaking": "Konuşmayı durdur",
"Start Speaking": "Konuşmaya başla",
"Speech": "Konuşma",
"Unhide": "Gizlemeyi bırak",
"Hide Others": "Diğerlerini gizle",
"Hide": "Gizle",
"Services": "Hizmetler",
"About": "Hakkında",
"Element Help": "Element yardımı",
"Help": "Yardım",
"Close": "Kapat",
"Minimize": "Küçült",
"Window": "Pencere",
"Toggle Developer Tools": "Geliştirici araçları",
"Toggle Full Screen": "Tam ekran",
"Preferences": "Tercihler",
"Zoom Out": "Uzaklaştır",
"Zoom In": "Yaklaştır",
"Actual Size": "Gerçek boyut",
"View": "Görünüm",
"Select All": "Tümünü seç",
"Delete": "Sil",
"Paste and Match Style": "Biçimiyle bir yapıştır",
"Paste": "Yapıştır",
"Copy": "Kopyala",
"Cut": "Kes",
"Redo": "Yinele",
"Undo": "Geri al",
"Edit": "Düzenle",
"Quit": ık",
"Show/Hide": "Göster/Gizle",
"Are you sure you want to quit?": ıkmak istediğinize emin misiniz?",
"Close Element": "Element'i kapat",
"Cancel": "İptal",
"Copy image address": "Görsel adresini kopyala"
}

View File

@@ -1,5 +1,5 @@
{
"Add to dictionary": "Thêm vào t điển",
"Add to dictionary": "Thêm vào t điển",
"The image failed to save": "Ảnh không lưu được",
"Failed to save image": "Lưu ảnh thất bại",
"Save image as...": "Lưu ảnh…",
@@ -7,27 +7,27 @@
"Copy email address": "Sao chép địa chỉ email",
"Copy image": "Sao chép ảnh",
"File": "Tệp",
"Bring All to Front": "Đưa tất cả lên trước",
"Zoom": "Thu phóng",
"Bring All to Front": "Mang tất cả ra trước",
"Zoom": "Phóng (to)",
"Stop Speaking": "Dừng nói",
"Start Speaking": "Bắt đầu nói",
"Speech": "Đọc màn hình",
"Speech": "Thoại",
"Unhide": "Bỏ ẩn",
"Hide Others": "Ẩn cái khác",
"Hide": "Ẩn",
"Services": "Dịch vụ",
"About": "Giới thiệu",
"Element Help": "Trợ giúp Element",
"About": "Về",
"Element Help": "Element hỗ trợ",
"Help": "Hỗ trợ",
"Close": "Đóng",
"Minimize": "Thu nhỏ",
"Window": "Cửa sổ",
"Toggle Developer Tools": "Công cụ cho Nhà phát triển",
"Toggle Full Screen": "Toàn màn hình",
"Toggle Developer Tools": "Chuyển Công cụ cho Nhà phát triển",
"Toggle Full Screen": "Chuyển toàn màn hình",
"Preferences": "Tùy chọn",
"Zoom Out": "Thu nhỏ",
"Zoom In": "Phóng to",
"Actual Size": "Kích thước thực",
"Actual Size": "Kích thước thực tế",
"View": "Xem",
"Select All": "Chọn tất cả",
"Delete": "Xóa",
@@ -37,11 +37,10 @@
"Cut": "Cắt",
"Redo": "Làm lại",
"Undo": "Hoàn tác",
"Edit": "Chỉnh sửa",
"Edit": "Biên tập",
"Quit": "Thoát",
"Show/Hide": "Hiển thị/Ẩn",
"Are you sure you want to quit?": "Bạn có chắc chắn muốn thoát?",
"Are you sure you want to quit?": "Bạn có chắc muốn thoát?",
"Close Element": "Đóng Element",
"Cancel": "Hủy bỏ",
"Copy image address": "Sao chép địa chỉ ảnh"
"Cancel": "Hủy"
}

View File

@@ -7,7 +7,7 @@
"Copy email address": "复制邮箱地址",
"Copy image": "复制图片",
"File": "文件",
"Bring All to Front": "全部置前",
"Bring All to Front": "置前",
"Zoom": "放大",
"Stop Speaking": "停止讲话",
"Start Speaking": "开始讲话",
@@ -22,9 +22,9 @@
"Close": "关闭",
"Minimize": "最小化",
"Window": "窗口",
"Toggle Developer Tools": "切换开发工具",
"Toggle Developer Tools": "切换开发工具",
"Toggle Full Screen": "切换全屏",
"Preferences": "偏好",
"Preferences": "外观",
"Zoom Out": "缩小",
"Zoom In": "放大",
"Actual Size": "实际大小",
@@ -34,7 +34,7 @@
"Paste and Match Style": "粘贴并匹配样式",
"Paste": "粘贴",
"Copy": "复制",
"Cut": "剪",
"Cut": "剪",
"Redo": "重做",
"Undo": "撤销",
"Edit": "编辑",
@@ -42,6 +42,5 @@
"Show/Hide": "显示/隐藏",
"Are you sure you want to quit?": "你确定要退出吗?",
"Close Element": "关闭 Element",
"Cancel": "取消",
"Copy image address": "复制图片地址"
"Cancel": "取消"
}

View File

@@ -42,7 +42,5 @@
"Show/Hide": "顯示/隱藏",
"Are you sure you want to quit?": "您確定要退出嗎?",
"Close Element": "關閉 Element",
"Cancel": "取消",
"Copy image address": "複製圖片地址",
"Close %(brand)s": "關閉 %(brand)s"
"Cancel": "取消"
}

View File

@@ -1,202 +0,0 @@
/*
Copyright 2022 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 { app, autoUpdater, desktopCapturer, ipcMain, powerSaveBlocker } from "electron";
import IpcMainEvent = Electron.IpcMainEvent;
import { recordSSOSession } from "./protocol";
import { randomArray } from "./utils";
import { Settings } from "./settings";
import { keytar } from "./keytar";
ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void {
if (process.platform !== 'win32') {
// only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron
// has some Windows support too, and in some Windows environments this leads to two badges rendering atop
// each other. See https://github.com/vector-im/element-web/issues/16942
app.badgeCount = count;
}
if (count === 0) {
global.mainWindow?.flashFrame(false);
}
});
let focusHandlerAttached = false;
ipcMain.on('loudNotification', function(): void {
if (process.platform === 'win32' && global.mainWindow && !global.mainWindow.isFocused() && !focusHandlerAttached) {
global.mainWindow.flashFrame(true);
global.mainWindow.once('focus', () => {
global.mainWindow.flashFrame(false);
focusHandlerAttached = false;
});
focusHandlerAttached = true;
}
});
let powerSaveBlockerId: number | null = null;
ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) {
switch (payload.action) {
case 'call_state': {
if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) {
if (payload.state === 'ended') {
powerSaveBlocker.stop(powerSaveBlockerId);
powerSaveBlockerId = null;
}
} else {
if (powerSaveBlockerId === null && payload.state === 'connected') {
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
}
}
break;
}
}
});
ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
if (!global.mainWindow) return;
const args = payload.args || [];
let ret: any;
switch (payload.name) {
case 'getUpdateFeedUrl':
ret = autoUpdater.getFeedURL();
break;
case 'getSettingValue': {
const [settingName] = args;
const setting = Settings[settingName];
ret = await setting.read();
break;
}
case 'setSettingValue': {
const [settingName, value] = args;
const setting = Settings[settingName];
await setting.write(value);
break;
}
case 'setLanguage':
global.appLocalization.setAppLocale(args[0]);
break;
case 'getAppVersion':
ret = app.getVersion();
break;
case 'focusWindow':
if (global.mainWindow.isMinimized()) {
global.mainWindow.restore();
} else if (!global.mainWindow.isVisible()) {
global.mainWindow.show();
} else {
global.mainWindow.focus();
}
break;
case 'getConfig':
ret = global.vectorConfig;
break;
case 'navigateBack':
if (global.mainWindow.webContents.canGoBack()) {
global.mainWindow.webContents.goBack();
}
break;
case 'navigateForward':
if (global.mainWindow.webContents.canGoForward()) {
global.mainWindow.webContents.goForward();
}
break;
case 'setSpellCheckEnabled':
if (typeof args[0] !== 'boolean') return;
global.mainWindow.webContents.session.setSpellCheckerEnabled(args[0]);
global.store.set("spellCheckerEnabled", args[0]);
break;
case 'getSpellCheckEnabled':
ret = global.store.get("spellCheckerEnabled", true);
break;
case 'setSpellCheckLanguages':
try {
global.mainWindow.webContents.session.setSpellCheckerLanguages(args[0]);
} catch (er) {
console.log("There were problems setting the spellcheck languages", er);
}
break;
case 'getSpellCheckLanguages':
ret = global.mainWindow.webContents.session.getSpellCheckerLanguages();
break;
case 'getAvailableSpellCheckLanguages':
ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages;
break;
case 'startSSOFlow':
recordSSOSession(args[0]);
break;
case 'getPickleKey':
try {
ret = await keytar?.getPassword("element.io", `${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 (e) {
// 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
ret = null;
}
break;
case 'createPickleKey':
try {
const pickleKey = await randomArray(32);
await keytar?.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey);
ret = pickleKey;
} catch (e) {
ret = null;
}
break;
case 'destroyPickleKey':
try {
await keytar?.deletePassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
await keytar?.deletePassword("riot.im", `${args[0]}|${args[1]}`);
} catch (e) {}
break;
case 'getDesktopCapturerSources':
ret = (await desktopCapturer.getSources(args[0])).map((source) => ({
id: source.id,
name: source.name,
thumbnailURL: source.thumbnail.toDataURL(),
}));
break;
default:
global.mainWindow.webContents.send('ipcReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
global.mainWindow.webContents.send('ipcReply', {
id: payload.id,
reply: ret,
});
});

View File

@@ -1,31 +0,0 @@
/*
Copyright 2022 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 type * as Keytar from "keytar"; // Hak dependency type
let keytar: typeof Keytar | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
keytar = require('keytar');
} catch (e) {
if ((<NodeJS.ErrnoException>e).code === "MODULE_NOT_FOUND") {
console.log("Keytar isn't installed; secure key storage is disabled.");
} else {
console.warn("Keytar unexpected error:", e);
}
}
export { keytar };

View File

@@ -15,10 +15,9 @@ limitations under the License.
*/
import counterpart from "counterpart";
import type Store from 'electron-store';
const FALLBACK_LOCALE = 'en';
const DEFAULT_LOCALE = "en";
export function _td(text: string): string {
return text;
@@ -27,12 +26,14 @@ export function _td(text: string): string {
type SubstitutionValue = number | string;
interface IVariables {
[key: string]: SubstitutionValue | undefined;
[key: string]: SubstitutionValue;
count?: number;
}
export function _t(text: string, variables: IVariables = {}): string {
const { count } = variables;
const args = Object.assign({ interpolate: false }, variables);
const { count } = args;
// Horrible hack to avoid https://github.com/vector-im/element-web/issues/4191
// The interpolation library that counterpart uses does not support undefined/null
@@ -41,20 +42,21 @@ export function _t(text: string, variables: IVariables = {}): string {
// valid ES6 template strings to i18n strings it's extremely easy to pass undefined/null
// if there are no existing null guards. To avoid this making the app completely inoperable,
// we'll check all the values for undefined/null and stringify them here.
Object.keys(variables).forEach((key) => {
if (variables[key] === undefined) {
Object.keys(args).forEach((key) => {
if (args[key] === undefined) {
console.warn("safeCounterpartTranslate called with undefined interpolation name: " + key);
variables[key] = 'undefined';
args[key] = 'undefined';
}
if (variables[key] === null) {
if (args[key] === null) {
console.warn("safeCounterpartTranslate called with null interpolation name: " + key);
variables[key] = 'null';
args[key] = 'null';
}
});
let translated = counterpart.translate(text, variables);
if (!translated && count !== undefined) {
// counterpart does not do fallback if no pluralisation exists in the preferred language, so do it here
translated = counterpart.translate(text, { ...variables, locale: FALLBACK_LOCALE });
let translated = counterpart.translate(text, args);
if (translated === undefined && count !== undefined) {
// counterpart does not do fallback if no pluralisation exists
// in the preferred language, so do it here
translated = counterpart.translate(text, Object.assign({}, args, { locale: DEFAULT_LOCALE }));
}
// The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution)
@@ -63,17 +65,17 @@ export function _t(text: string, variables: IVariables = {}): string {
type Component = () => void;
type TypedStore = Store<{ locale?: string[] }>;
type TypedStore = Store<{ locale?: string | string[] }>;
export class AppLocalization {
private static readonly STORE_KEY = "locale";
private readonly store: TypedStore;
private readonly localizedComponents?: Set<Component>;
private readonly localizedComponents: Set<Component>;
constructor({ store, components = [] }: { store: TypedStore, components: Component[] }) {
counterpart.registerTranslations(FALLBACK_LOCALE, this.fetchTranslationJson("en_EN"));
counterpart.setFallbackLocale(FALLBACK_LOCALE);
counterpart.registerTranslations("en", this.fetchTranslationJson("en_EN"));
counterpart.setFallbackLocale('en');
counterpart.setSeparator('|');
if (Array.isArray(components)) {
@@ -83,32 +85,19 @@ export class AppLocalization {
this.store = store;
if (this.store.has(AppLocalization.STORE_KEY)) {
const locales = this.store.get(AppLocalization.STORE_KEY);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.setAppLocale(locales!);
this.setAppLocale(locales);
}
this.resetLocalizedUI();
}
// Format language strings from normalized form to non-normalized form (e.g. en-gb to en_GB)
private denormalize(locale: string): string {
if (locale === "en") {
locale = "en_EN";
}
const parts = locale.split("-");
if (parts.length > 1) {
parts[1] = parts[1].toUpperCase();
}
return parts.join("_");
}
public fetchTranslationJson(locale: string): Record<string, string> {
try {
console.log("Fetching translation json for locale: " + locale);
return require(`./i18n/strings/${this.denormalize(locale)}.json`);
return require(`./i18n/strings/${locale}.json`);
} catch (e) {
console.log(`Could not fetch translation json for locale: '${locale}'`, e);
return {};
return null;
}
}
@@ -119,15 +108,16 @@ export class AppLocalization {
locales = [locales];
}
const loadedLocales = locales.filter(locale => {
locales.forEach(locale => {
const translations = this.fetchTranslationJson(locale);
if (translations !== null) {
counterpart.registerTranslations(locale, translations);
}
return !!translations;
});
counterpart.setLocale(loadedLocales[0]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - this looks like a bug but is out of scope for this conversion
counterpart.setLocale(locales);
this.store.set(AppLocalization.STORE_KEY, locales);
this.resetLocalizedUI();
@@ -135,7 +125,7 @@ export class AppLocalization {
public resetLocalizedUI(): void {
console.log("Resetting the UI components after locale change");
this.localizedComponents?.forEach(componentSetup => {
this.localizedComponents.forEach(componentSetup => {
if (typeof componentSetup === "function") {
componentSetup();
}

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ipcRenderer, contextBridge, IpcRendererEvent } from 'electron';
import { ipcRenderer, desktopCapturer, contextBridge, IpcRendererEvent, SourcesOptions } from 'electron';
// Expose only expected IPC wrapper APIs to the renderer process to avoid
// handing out generalised messaging access.
@@ -33,9 +33,15 @@ const CHANNELS = [
"setBadgeCount",
"update-downloaded",
"userDownloadCompleted",
"userDownloadAction",
"userDownloadOpen",
];
interface ISource {
id: string;
name: string;
thumbnailURL: string;
}
contextBridge.exposeInMainWorld(
"electron",
{
@@ -53,5 +59,19 @@ contextBridge.exposeInMainWorld(
}
ipcRenderer.send(channel, ...args);
},
async getDesktopCapturerSources(options: SourcesOptions): Promise<ISource[]> {
const sources = await desktopCapturer.getSources(options);
const desktopCapturerSources: ISource[] = [];
for (const source of sources) {
desktopCapturerSources.push({
id: source.id,
name: source.name,
thumbnailURL: source.thumbnail.toDataURL(),
});
}
return desktopCapturerSources;
},
},
);

View File

@@ -80,13 +80,13 @@ export function recordSSOSession(sessionID: string): void {
writeStore(store);
}
export function getProfileFromDeeplink(args: string[]): string | undefined {
export function getProfileFromDeeplink(args): string | undefined {
// check if we are passed a profile in the SSO callback url
const deeplinkUrl = args.find(arg => arg.startsWith(PROTOCOL + '//'));
if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) {
const parsedUrl = new URL(deeplinkUrl);
if (parsedUrl.protocol === PROTOCOL) {
const ssoID = parsedUrl.searchParams.get(SEARCH_PARAM)!;
const ssoID = parsedUrl.searchParams.get(SEARCH_PARAM);
const store = readStore();
console.log("Forwarding to profile: ", store[ssoID]);
return store[ssoID];

View File

@@ -1,324 +0,0 @@
/*
Copyright 2022 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 { app, ipcMain } from "electron";
import { promises as afs } from "fs";
import path from "path";
import type {
Seshat as SeshatType,
SeshatRecovery as SeshatRecoveryType,
ReindexError as ReindexErrorType,
} from "matrix-seshat"; // Hak dependency type
import IpcMainEvent = Electron.IpcMainEvent;
import { randomArray } from "./utils";
import { keytar } from "./keytar";
let seshatSupported = false;
let Seshat: typeof SeshatType;
let SeshatRecovery: typeof SeshatRecoveryType;
let ReindexError: typeof ReindexErrorType;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const seshatModule = require('matrix-seshat');
Seshat = seshatModule.Seshat;
SeshatRecovery = seshatModule.SeshatRecovery;
ReindexError = seshatModule.ReindexError;
seshatSupported = true;
} catch (e) {
if ((<NodeJS.ErrnoException>e).code === "MODULE_NOT_FOUND") {
console.log("Seshat isn't installed, event indexing is disabled.");
} else {
console.warn("Seshat unexpected error:", e);
}
}
const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
let eventIndex: SeshatType | null = null;
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
async function getOrCreatePassphrase(key: string): Promise<string> {
if (keytar) {
try {
const storedPassphrase = await keytar.getPassword("element.io", key);
if (storedPassphrase !== null) {
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);
}
}
return seshatDefaultPassphrase;
}
const deleteContents = async (p: string): Promise<void> => {
for (const entry of await afs.readdir(p)) {
const curPath = path.join(p, entry);
await afs.unlink(curPath);
}
};
ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
if (!global.mainWindow) return;
const sendError = (id, e) => {
const error = {
message: e.message,
};
global.mainWindow.webContents.send('seshatReply', {
id: id,
error: error,
});
};
const args = payload.args || [];
let ret: any;
switch (payload.name) {
case 'supportsEventIndexing':
ret = seshatSupported;
break;
case 'initEventIndex':
if (eventIndex === null) {
const userId = args[0];
const deviceId = args[1];
const passphraseKey = `seshat|${userId}|${deviceId}`;
const passphrase = await getOrCreatePassphrase(passphraseKey);
try {
await afs.mkdir(eventStorePath, { recursive: true });
eventIndex = new Seshat(eventStorePath, { passphrase });
} catch (e) {
if (e instanceof ReindexError) {
// If this is a reindex error, the index schema
// changed. Try to open the database in recovery mode,
// reindex the database and finally try to open the
// database again.
const recoveryIndex = new SeshatRecovery(eventStorePath, {
passphrase,
});
const userVersion = await recoveryIndex.getUserVersion();
// If our user version is 0 we'll delete the db
// anyways so reindexing it is a waste of time.
if (userVersion === 0) {
await recoveryIndex.shutdown();
try {
await deleteContents(eventStorePath);
} catch (e) {
}
} else {
await recoveryIndex.reindex();
}
eventIndex = new Seshat(eventStorePath, { passphrase });
} else {
sendError(payload.id, e);
return;
}
}
}
break;
case 'closeEventIndex':
if (eventIndex !== null) {
const index = eventIndex;
eventIndex = null;
try {
await index.shutdown();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'deleteEventIndex': {
try {
await deleteContents(eventStorePath);
} catch (e) {
}
break;
}
case 'isEventIndexEmpty':
if (eventIndex === null) ret = true;
else ret = await eventIndex.isEmpty();
break;
case 'isRoomIndexed':
if (eventIndex === null) ret = false;
else ret = await eventIndex.isRoomIndexed(args[0]);
break;
case 'addEventToIndex':
try {
eventIndex?.addEvent(args[0], args[1]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'deleteEvent':
try {
ret = await eventIndex?.deleteEvent(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'commitLiveEvents':
try {
ret = await eventIndex?.commit();
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'searchEventIndex':
try {
ret = await eventIndex?.search(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'addHistoricEvents':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addHistoricEvents(
args[0], args[1], args[2]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getStats':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getStats();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'removeCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.removeCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'addCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadFileEvents':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadFileEvents(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadCheckpoints':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadCheckpoints();
} catch (e) {
ret = [];
}
}
break;
case 'setUserVersion':
if (eventIndex === null) break;
else {
try {
await eventIndex.setUserVersion(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getUserVersion':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getUserVersion();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
default:
global.mainWindow.webContents.send('seshatReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
global.mainWindow.webContents.send('seshatReply', {
id: payload.id,
reply: ret,
});
});

View File

@@ -1,77 +0,0 @@
/*
Copyright 2022 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 * as tray from "./tray";
interface Setting {
read(): Promise<any>;
write(value: any): Promise<void>;
}
export const Settings: Record<string, Setting> = {
"Electron.autoLaunch": {
async read(): Promise<any> {
return global.launcher.isEnabled();
},
async write(value: any): Promise<void> {
if (value) {
return global.launcher.enable();
} else {
return global.launcher.disable();
}
},
},
"Electron.warnBeforeExit": {
async read(): Promise<any> {
return global.store.get("warnBeforeExit", true);
},
async write(value: any): Promise<void> {
global.store.set("warnBeforeExit", value);
},
},
"Electron.alwaysShowMenuBar": { // not supported on macOS
async read(): Promise<any> {
return !global.mainWindow.autoHideMenuBar;
},
async write(value: any): Promise<void> {
global.store.set('autoHideMenuBar', !value);
global.mainWindow.autoHideMenuBar = !value;
global.mainWindow.setMenuBarVisibility(value);
},
},
"Electron.showTrayIcon": { // not supported on macOS
async read(): Promise<any> {
return tray.hasTray();
},
async write(value: any): Promise<void> {
if (value) {
// Create trayIcon icon
tray.create(global.trayConfig);
} else {
tray.destroy();
}
global.store.set('minimizeToTray', value);
},
},
"Electron.enableHardwareAcceleration": {
async read(): Promise<any> {
return !global.store.get('disableHardwareAcceleration', false);
},
async write(value: any): Promise<void> {
global.store.set('disableHardwareAcceleration', !value);
},
},
};

View File

@@ -17,6 +17,7 @@ limitations under the License.
import path from "path";
import { spawn } from "child_process";
import { app } from "electron";
import { promises as fsProm } from "fs";
function runUpdateExe(args: string[]): Promise<void> {
// Invokes Squirrel's Update.exe which will do things for us like create shortcuts
@@ -34,16 +35,44 @@ function runUpdateExe(args: string[]): Promise<void> {
function checkSquirrelHooks(): boolean {
if (process.platform !== 'win32') return false;
const cmd = process.argv[1];
const target = path.basename(process.execPath);
if (cmd === '--squirrel-install') {
runUpdateExe(['--createShortcut=' + target]).then(() => app.quit());
return true;
} else if (cmd === '--squirrel-updated') {
app.quit();
if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') {
runUpdateExe(['--createShortcut=' + target]).then(() => {
// remove the old 'Riot' shortcuts, if they exist (update.exe --removeShortcut doesn't work
// because it always uses the name of the product as the name of the shortcut: the only variable
// is what executable you're linking to)
const appDataDir = process.env.APPDATA;
if (!appDataDir) return;
const startMenuDir = path.join(
appDataDir, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'New Vector Ltd',
);
return fsProm.rmdir(startMenuDir, { recursive: true });
}).then(() => {
// same for 'Element (Riot) which is old now too (we have to try to delete both because
// we don't know what version we're updating from, but of course we do know this version
// is 'Element' so the two old ones are all safe to delete).
const appDataDir = process.env.APPDATA;
if (!appDataDir) return;
const oldStartMenuLink = path.join(
appDataDir, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Element', 'Element (Riot).lnk',
);
return fsProm.unlink(oldStartMenuLink).catch(() => {});
}).then(() => {
const oldDesktopShortcut = path.join(app.getPath('desktop'), 'Element (Riot).lnk');
return fsProm.unlink(oldDesktopShortcut).catch(() => {});
}).then(() => {
const oldDesktopShortcut = path.join(app.getPath('desktop'), 'Riot.lnk');
return fsProm.unlink(oldDesktopShortcut).catch(() => {});
}).then(() => {
app.quit();
});
return true;
} else if (cmd === '--squirrel-uninstall') {
runUpdateExe(['--removeShortcut=' + target]).then(() => app.quit());
runUpdateExe(['--removeShortcut=' + target]).then(() => {
app.quit();
});
return true;
} else if (cmd === '--squirrel-obsolete') {
app.quit();

View File

@@ -19,10 +19,9 @@ import { app, Tray, Menu, nativeImage } from "electron";
import pngToIco from "png-to-ico";
import path from "path";
import fs from "fs";
import { _t } from "./language-helper";
let trayIcon: Tray | null = null;
let trayIcon: Tray = null;
export function hasTray(): boolean {
return (trayIcon !== null);
@@ -36,7 +35,7 @@ export function destroy(): void {
}
function toggleWin(): void {
if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized() && global.mainWindow.isFocused()) {
if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) {
global.mainWindow.hide();
} else {
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
@@ -65,7 +64,7 @@ export function create(config: IConfig): void {
if (!favicons || favicons.length <= 0 || !favicons[0].startsWith('data:')) {
if (lastFavicon !== null) {
global.mainWindow.setIcon(defaultIcon);
trayIcon?.setImage(defaultIcon);
trayIcon.setImage(defaultIcon);
lastFavicon = null;
}
return;
@@ -88,12 +87,12 @@ export function create(config: IConfig): void {
}
}
trayIcon?.setImage(newFavicon);
trayIcon.setImage(newFavicon);
global.mainWindow.setIcon(newFavicon);
});
global.mainWindow.webContents.on('page-title-updated', function(ev, title) {
trayIcon?.setToolTip(title);
trayIcon.setToolTip(title);
});
}

View File

@@ -28,17 +28,7 @@ function installUpdate(): void {
function pollForUpdates(): void {
try {
// If we've already got a new update downloaded, then stop trying to check for new ones, as according to the doc
// at https://github.com/electron/electron/blob/main/docs/api/auto-updater.md#autoupdatercheckforupdates
// we'll just keep re-downloading the same update.
// As a hunch, this might also be causing https://github.com/vector-im/element-web/issues/12433
// due to the update checks colliding with the pending install somehow
if (!latestUpdateDownloaded) {
autoUpdater.checkForUpdates();
} else {
console.log("Skipping update check as download already present");
global.mainWindow?.webContents.send('update-downloaded', latestUpdateDownloaded);
}
autoUpdater.checkForUpdates();
} catch (e) {
console.log('Couldn\'t check for update', e);
}
@@ -49,7 +39,7 @@ export function start(updateBaseUrl: string): void {
updateBaseUrl = updateBaseUrl + '/';
}
try {
let url: string;
let url;
// For reasons best known to Squirrel, the way it checks for updates
// is completely different between macOS and windows. On macOS, it
// hits a URL that either gives it a 200 with some json or
@@ -70,12 +60,11 @@ export function start(updateBaseUrl: string): void {
// I'm not even going to try to guess which feed style they'd use if they
// implemented it on Linux, or if it would be different again.
console.log('Auto update not supported on this platform');
return;
}
if (url) {
console.log(`Update URL: ${url}`);
autoUpdater.setFeedURL({ url });
autoUpdater.setFeedURL(url);
// We check for updates ourselves rather than using 'updater' because we need to
// do it in the main process (and we don't really need to check every 10 minutes:
// every hour should be just fine for a desktop app)
@@ -96,7 +85,8 @@ ipcMain.on('install_update', installUpdate);
ipcMain.on('check_updates', pollForUpdates);
function ipcChannelSendUpdateStatus(status: boolean | string): void {
global.mainWindow?.webContents.send('check_updates', status);
if (!global.mainWindow) return;
global.mainWindow.webContents.send('check_updates', status);
}
interface ICachedUpdate {
@@ -115,7 +105,8 @@ autoUpdater.on('update-available', function() {
// the only time we will get `update-not-available` if `latestUpdateDownloaded` is already set
// is if the user used the Manual Update check and there is no update newer than the one we
// have downloaded, so show it to them as the latest again.
global.mainWindow?.webContents.send('update-downloaded', latestUpdateDownloaded);
if (!global.mainWindow) return;
global.mainWindow.webContents.send('update-downloaded', latestUpdateDownloaded);
} else {
ipcChannelSendUpdateStatus(false);
}
@@ -124,7 +115,8 @@ autoUpdater.on('update-available', function() {
});
autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName, releaseDate, updateURL) => {
if (!global.mainWindow) return;
// forward to renderer
latestUpdateDownloaded = { releaseNotes, releaseName, releaseDate, updateURL };
global.mainWindow?.webContents.send('update-downloaded', latestUpdateDownloaded);
global.mainWindow.webContents.send('update-downloaded', latestUpdateDownloaded);
});

View File

@@ -1,29 +0,0 @@
/*
Copyright 2022 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 crypto from "crypto";
export async function randomArray(size: number): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, buf) => {
if (err) {
reject(err);
} else {
resolve(buf.toString("base64").replace(/=+$/g, ''));
}
});
});
}

View File

@@ -15,7 +15,6 @@ limitations under the License.
*/
import { app, shell, Menu, MenuItem, MenuItemConstructorOptions } from 'electron';
import { _t } from './language-helper';
const isMac = process.platform === 'darwin';

Some files were not shown because too many files have changed in this diff Show More