mirror of
https://github.com/element-hq/element-desktop.git
synced 2026-01-10 08:29:49 -05:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e3fa1652 | ||
|
|
86891dcdd4 | ||
|
|
55ee9c0053 | ||
|
|
1fd2214951 | ||
|
|
fe253172b3 | ||
|
|
c7cdbcf07b | ||
|
|
5e882f8e08 | ||
|
|
e5f6bd882f | ||
|
|
e4f75de04c | ||
|
|
1c55aa780f | ||
|
|
5dd1984896 | ||
|
|
3c78634cd5 | ||
|
|
155bbe3634 | ||
|
|
a64ed5428c | ||
|
|
d0a735b25c | ||
|
|
522c6f95ab | ||
|
|
155b6f284a | ||
|
|
8376cef25a | ||
|
|
15f23d4cf9 | ||
|
|
ea2b160dcc | ||
|
|
bc0a8f03db | ||
|
|
39eddfdc4b | ||
|
|
e668edbcec | ||
|
|
763ebc4ca8 | ||
|
|
cfff1c7640 | ||
|
|
ec1971366a | ||
|
|
1f803f7051 | ||
|
|
c90c79e3ae | ||
|
|
22100bff80 | ||
|
|
a4b622dd5e | ||
|
|
c9a03859bd | ||
|
|
2385c4dd9b | ||
|
|
9d2b1621d6 | ||
|
|
35db60092b | ||
|
|
0a97f12323 | ||
|
|
cd671919e2 | ||
|
|
8e964cd782 | ||
|
|
64d8341deb |
2
.github/workflows/build_and_test.yaml
vendored
2
.github/workflows/build_and_test.yaml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
tests-done:
|
||||
needs: [windows, linux, macos]
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
|
||||
72
.github/workflows/build_linux.yaml
vendored
72
.github/workflows/build_linux.yaml
vendored
@@ -39,15 +39,39 @@ on:
|
||||
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
|
||||
for example icons in the `build/` directory to override the app icons.
|
||||
default: "webapp"
|
||||
test:
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: "Whether to run the test stage after building"
|
||||
test-args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Additional arguments to pass to playwright"
|
||||
runs-on:
|
||||
type: string
|
||||
required: false
|
||||
description: "The runner image to use, normally set for you, may be needed for running in private repos."
|
||||
artifact-prefix:
|
||||
type: string
|
||||
required: false
|
||||
description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos."
|
||||
default: ""
|
||||
targets:
|
||||
type: string
|
||||
required: false
|
||||
description: "List of targets to build"
|
||||
default: "tar.gz deb"
|
||||
env:
|
||||
SQLCIPHER_BUNDLED: ${{ inputs.sqlcipher == 'static' && '1' || '' }}
|
||||
MAX_GLIBC: 2.31 # bullseye-era glibc, used by glibc-check.sh
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
build:
|
||||
name: Build Linux ${{ inputs.arch }} SQLCipher ${{ inputs.sqlcipher }}
|
||||
# We build on native infrastructure as matrix-seshat fails to cross-compile properly
|
||||
# https://github.com/matrix-org/seshat/issues/135
|
||||
runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
|
||||
runs-on: ${{ inputs.runs-on || (inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04') }}
|
||||
env:
|
||||
HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-desktop-dockerbuild
|
||||
steps:
|
||||
@@ -90,7 +114,7 @@ jobs:
|
||||
|
||||
- name: Cache .hak
|
||||
id: cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
key: ${{ runner.os }}-${{ github.ref_name }}-${{ inputs.sqlcipher }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion', 'dockerbuild/*') }}
|
||||
path: |
|
||||
@@ -109,7 +133,7 @@ jobs:
|
||||
|
||||
- name: "Get modified files"
|
||||
id: changed_files
|
||||
if: steps.cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request'
|
||||
if: steps.cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request' && github.repository == 'element-hq/element-desktop'
|
||||
uses: tj-actions/changed-files@823fcebdb31bb35fdf2229d9f769b400309430d0 # v46
|
||||
with:
|
||||
files: |
|
||||
@@ -152,19 +176,20 @@ jobs:
|
||||
fi
|
||||
|
||||
# Workaround for https://github.com/electron-userland/electron-builder/issues/6116
|
||||
# and https://github.com/electron-userland/electron-builder/issues/5721
|
||||
- name: Install fpm
|
||||
if: inputs.arch == 'arm64'
|
||||
run: |
|
||||
sudo apt-get install ruby-dev build-essential
|
||||
sudo gem install fpm
|
||||
echo "USE_SYSTEM_FPM=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Build App
|
||||
run: yarn build --publish never -l ${{ steps.config.outputs.build-args }}
|
||||
run: yarn build --publish never ${{ steps.config.outputs.build-args }} -l ${{ inputs.targets }}
|
||||
env:
|
||||
VARIANT_PATH: variant.json
|
||||
# Only set for Nightly builds
|
||||
VERSION: ${{ inputs.version }}
|
||||
# Workaround for https://github.com/electron-userland/electron-builder/issues/5721
|
||||
USE_HARD_LINKS: false
|
||||
|
||||
- name: Check native libraries
|
||||
run: |
|
||||
@@ -172,11 +197,9 @@ jobs:
|
||||
shopt -s globstar
|
||||
|
||||
FILES=$(file dist/**/*.node)
|
||||
echo "$FILES"
|
||||
echo $FILES
|
||||
|
||||
if [ grep -v "$ARCH" ]; then
|
||||
exit 1
|
||||
fi
|
||||
! echo "$FILES" | grep -v "$ARCH"
|
||||
|
||||
LIBS=$(readelf -d dist/**/*.node | grep NEEDED)
|
||||
echo "$LIBS"
|
||||
@@ -198,27 +221,46 @@ jobs:
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
|
||||
name: ${{ inputs.artifact-prefix }}linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
|
||||
path: |
|
||||
dist
|
||||
!dist/*-unpacked/**
|
||||
retention-days: 1
|
||||
|
||||
- name: Assert all required files are present
|
||||
- name: Assert deb is present and valid
|
||||
if: contains(inputs.targets, 'deb')
|
||||
run: |
|
||||
test -f ./dist/element-desktop*$ARCH.deb
|
||||
test -f ./dist/element-desktop*.tar.gz
|
||||
|
||||
DEB_LISTING=$(dpkg-deb --fsys-tarfile ./dist/element-desktop*.deb | tar -tv)
|
||||
echo "deb listing: "
|
||||
echo "$DEB_LISTING"
|
||||
! echo "$DEB_LISTING" | grep '^h'
|
||||
env:
|
||||
ARCH: ${{ inputs.arch }}
|
||||
|
||||
- name: Assert tar.gz is present
|
||||
if: contains(inputs.targets, 'tar.gz')
|
||||
run: |
|
||||
test -f ./dist/element-desktop*.tar.gz
|
||||
|
||||
TAR_GZ_LISTING=$(tar -tvf ./dist/element-desktop*.tar.gz)
|
||||
echo "tar.gz listing: "
|
||||
echo "$TAR_GZ_LISTING"
|
||||
! echo "$TAR_GZ_LISTING" | grep '^h'
|
||||
|
||||
test:
|
||||
name: Test Linux ${{ inputs.arch }} SQLCipher ${{ inputs.sqlcipher }}
|
||||
needs: build
|
||||
if: inputs.test && contains(inputs.targets, 'deb')
|
||||
uses: ./.github/workflows/build_test.yaml
|
||||
with:
|
||||
artifact: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
|
||||
runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
|
||||
project: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
|
||||
artifact: ${{ inputs.artifact-prefix }}linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
|
||||
runs-on: ${{ inputs.runs-on || (inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04') }}
|
||||
executable: /opt/Element*/element-desktop*
|
||||
prepare_cmd: |
|
||||
sudo apt-get -qq update
|
||||
sudo apt install ./dist/*.deb
|
||||
blob_report: ${{ inputs.blob_report }}
|
||||
args: ${{ inputs.test-args }}
|
||||
|
||||
43
.github/workflows/build_macos.yaml
vendored
43
.github/workflows/build_macos.yaml
vendored
@@ -49,9 +49,29 @@ on:
|
||||
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
|
||||
for example icons in the `build/` directory to override the app icons.
|
||||
default: "webapp"
|
||||
test:
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: "Whether to run the test stage after building"
|
||||
test-args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Additional arguments to pass to playwright"
|
||||
artifact-prefix:
|
||||
type: string
|
||||
required: false
|
||||
description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos."
|
||||
default: ""
|
||||
targets:
|
||||
type: string
|
||||
required: false
|
||||
description: "List of targets to build"
|
||||
default: "dmg zip"
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
build:
|
||||
name: Build macOS Universal
|
||||
runs-on: macos-14 # M1
|
||||
environment: ${{ inputs.sign && 'packages.element.io' || '' }}
|
||||
steps:
|
||||
@@ -66,7 +86,7 @@ jobs:
|
||||
|
||||
- name: Cache .hak
|
||||
id: cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
key: ${{ runner.os }}-${{ hashFiles('hakHash', 'electronVersion') }}
|
||||
path: |
|
||||
@@ -105,7 +125,7 @@ jobs:
|
||||
- name: "[Signed] Build App"
|
||||
if: inputs.sign != ''
|
||||
run: |
|
||||
yarn build:universal --publish never
|
||||
yarn build:universal --publish never -m ${{ inputs.targets }}
|
||||
env:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
@@ -127,7 +147,7 @@ jobs:
|
||||
- name: "[Unsigned] Build App"
|
||||
if: inputs.sign == ''
|
||||
run: |
|
||||
yarn build:universal --publish never
|
||||
yarn build:universal --publish never -m ${{ inputs.targets }}
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||
VARIANT_PATH: variant.json
|
||||
@@ -162,22 +182,30 @@ jobs:
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: macos
|
||||
name: ${{ inputs.artifact-prefix }}macos
|
||||
path: |
|
||||
dist
|
||||
!dist/mac-universal/**
|
||||
retention-days: 1
|
||||
|
||||
- name: Assert all required files are present
|
||||
- name: Assert zip is present
|
||||
if: contains(inputs.targets, 'zip')
|
||||
run: |
|
||||
test -f ./dist/Element*.dmg
|
||||
test -f ./dist/Element*-mac.zip
|
||||
|
||||
- name: Assert dmg is present
|
||||
if: contains(inputs.targets, 'dmg')
|
||||
run: |
|
||||
test -f ./dist/Element*.dmg
|
||||
|
||||
test:
|
||||
name: Test macOS Universal
|
||||
needs: build
|
||||
if: inputs.test && contains(inputs.targets, 'dmg')
|
||||
uses: ./.github/workflows/build_test.yaml
|
||||
with:
|
||||
artifact: macos
|
||||
project: macos
|
||||
artifact: ${{ inputs.artifact-prefix }}macos
|
||||
runs-on: macos-14
|
||||
executable: /Users/runner/Applications/Element*.app/Contents/MacOS/Element*
|
||||
# We need to mount the DMG and copy the app to the Applications folder as a mounted DMG is
|
||||
@@ -187,3 +215,4 @@ jobs:
|
||||
rsync -a /Volumes/Element/Element*.app ~/Applications/ &&
|
||||
hdiutil detach /Volumes/Element
|
||||
blob_report: ${{ inputs.blob_report }}
|
||||
args: ${{ inputs.test-args }}
|
||||
|
||||
13
.github/workflows/build_test.yaml
vendored
13
.github/workflows/build_test.yaml
vendored
@@ -10,6 +10,10 @@ on:
|
||||
type: string
|
||||
required: true
|
||||
description: "The name of the artifact to download"
|
||||
project:
|
||||
type: string
|
||||
required: true
|
||||
description: "The Playwright project to use for testing"
|
||||
executable:
|
||||
type: string
|
||||
required: true
|
||||
@@ -22,12 +26,19 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
description: "Whether to upload a blob report instead of the HTML report"
|
||||
args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Additional arguments to pass to playwright, for e.g. skipping specific tests"
|
||||
permissions: {}
|
||||
jobs:
|
||||
test:
|
||||
name: Test ${{ inputs.project }}
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: ${{ github.repository == 'element-hq/element-web-pro' && 'element-hq/element-desktop' || github.repository }}
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
@@ -69,7 +80,7 @@ jobs:
|
||||
uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a
|
||||
timeout-minutes: 20
|
||||
with:
|
||||
run: yarn test --project=${{ inputs.artifact }} ${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} ${{ inputs.blob_report == false && '--reporter=html' || '' }}
|
||||
run: yarn test --project=${{ inputs.project }} ${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} ${{ inputs.blob_report == false && '--reporter=html' || '' }} ${{ inputs.args }}
|
||||
env:
|
||||
ELEMENT_DESKTOP_EXECUTABLE: ${{ steps.executable.outputs.path }}
|
||||
|
||||
|
||||
48
.github/workflows/build_windows.yaml
vendored
48
.github/workflows/build_windows.yaml
vendored
@@ -53,9 +53,33 @@ on:
|
||||
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
|
||||
for example icons in the `build/` directory to override the app icons.
|
||||
default: "webapp"
|
||||
test:
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: "Whether to run the test stage after building"
|
||||
test-runs-on:
|
||||
type: string
|
||||
required: false
|
||||
description: "The runner image to use for testing, normally set for you, may be needed for running in private repos."
|
||||
test-args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Additional arguments to pass to playwright"
|
||||
artifact-prefix:
|
||||
type: string
|
||||
required: false
|
||||
description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos."
|
||||
default: ""
|
||||
targets:
|
||||
type: string
|
||||
required: false
|
||||
description: "List of targets to build"
|
||||
default: "squirrel msi"
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
build:
|
||||
name: Build Windows ${{ inputs.arch }}
|
||||
runs-on: windows-2025
|
||||
environment: ${{ inputs.sign && 'packages.element.io' || '' }}
|
||||
env:
|
||||
@@ -95,7 +119,7 @@ jobs:
|
||||
|
||||
- name: Cache .hak
|
||||
id: cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
key: ${{ runner.os }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion') }}
|
||||
path: |
|
||||
@@ -208,7 +232,7 @@ jobs:
|
||||
MASTER_KEY_FILE: C:\Users\runneradmin\eSignerCKA\master.key
|
||||
|
||||
- name: Build App
|
||||
run: yarn build --publish never -w ${{ steps.config.outputs.build-args }}
|
||||
run: yarn build --publish never ${{ steps.config.outputs.build-args }} -w ${{ inputs.targets }}
|
||||
env:
|
||||
VARIANT_PATH: variant.json
|
||||
# Only set for Nightly builds
|
||||
@@ -236,24 +260,36 @@ jobs:
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: win-${{ inputs.arch }}
|
||||
name: ${{ inputs.artifact-prefix }}win-${{ inputs.arch }}
|
||||
path: |
|
||||
dist
|
||||
retention-days: 1
|
||||
|
||||
- name: Assert all required files are present
|
||||
- name: Assert executable is present
|
||||
run: |
|
||||
Test-Path './dist/win-*unpacked/Element*.exe'
|
||||
|
||||
- name: Assert all Squirrel files are present
|
||||
if: contains(inputs.targets, 'squirrel')
|
||||
run: |
|
||||
Test-Path './dist/squirrel-windows*/Element Setup*.exe'
|
||||
Test-Path './dist/squirrel-windows*/element-desktop-*-full.nupkg'
|
||||
Test-Path './dist/squirrel-windows*/RELEASES'
|
||||
|
||||
- name: Assert MSI is present
|
||||
if: contains(inputs.targets, 'msi')
|
||||
run: |
|
||||
Test-Path './dist/Element*.msi'
|
||||
|
||||
test:
|
||||
name: Test Windows ${{ inputs.arch }}
|
||||
needs: build
|
||||
if: inputs.test
|
||||
uses: ./.github/workflows/build_test.yaml
|
||||
with:
|
||||
artifact: win-${{ inputs.arch }}
|
||||
runs-on: ${{ inputs.arch == 'arm64' && 'windows-11-arm' || 'windows-2022' }}
|
||||
project: win-${{ inputs.arch }}
|
||||
artifact: ${{ inputs.artifact-prefix }}win-${{ inputs.arch }}
|
||||
runs-on: ${{ inputs.test-runs-on || (inputs.arch == 'arm64' && 'windows-11-arm' || 'windows-2022') }}
|
||||
executable: ./dist/win*-unpacked/Element*.exe
|
||||
blob_report: ${{ inputs.blob_report }}
|
||||
args: ${{ inputs.test-args }}
|
||||
|
||||
2
.github/workflows/dockerbuild.yaml
vendored
2
.github/workflows/dockerbuild.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
run: docker run -v $PWD:/project element-desktop-dockerbuild yarn install
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v22.19.0
|
||||
22.20.0
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,3 +1,29 @@
|
||||
Changes in [1.12.2](https://github.com/element-hq/element-desktop/releases/tag/v1.12.2) (2025-10-21)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Allow Desktop app to be auto-started minimised or focused ([#2622](https://github.com/element-hq/element-desktop/pull/2622)). Contributed by @t3chguy.
|
||||
* Room List: Extend the viewport to avoid so many black spots when scrolling the room list ([#30867](https://github.com/element-hq/element-web/pull/30867)). Contributed by @langleyd.
|
||||
* Hide calling buttons in room header before a room is created ([#30816](https://github.com/element-hq/element-web/pull/30816)). Contributed by @Half-Shot.
|
||||
* Improve invite dialog ui - Part 2 ([#30836](https://github.com/element-hq/element-web/pull/30836)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix hardlinks appearing in and breaking deb packages ([#2609](https://github.com/element-hq/element-desktop/pull/2609)). Contributed by @t3chguy.
|
||||
* Fix platform settings race condition and make auto-launch tri-state ([#30977](https://github.com/element-hq/element-web/pull/30977)). Contributed by @t3chguy.
|
||||
* Fix: member count in header and member list ([#30982](https://github.com/element-hq/element-web/pull/30982)). Contributed by @florianduros.
|
||||
* Fix duration of voice message in timeline ([#30973](https://github.com/element-hq/element-web/pull/30973)). Contributed by @florianduros.
|
||||
* Fix voice notes rendering at 00:00 when playback had not begun. ([#30961](https://github.com/element-hq/element-web/pull/30961)). Contributed by @Half-Shot.
|
||||
* Improve handling of animated images, add support for AVIF animations ([#30932](https://github.com/element-hq/element-web/pull/30932)). Contributed by @t3chguy.
|
||||
* Update key storage toggle when key storage status changes ([#30934](https://github.com/element-hq/element-web/pull/30934)). Contributed by @uhoreg.
|
||||
* Fix jitsi widget popout ([#30908](https://github.com/element-hq/element-web/pull/30908)). Contributed by @dbkr.
|
||||
* Improve keyboard navigation on invite dialog ([#30930](https://github.com/element-hq/element-web/pull/30930)). Contributed by @florianduros.
|
||||
* Prefer UIA flows with supported UIA stages ([#30926](https://github.com/element-hq/element-web/pull/30926)). Contributed by @richvdh.
|
||||
* Enhance accessibility of dropdown ([#30928](https://github.com/element-hq/element-web/pull/30928)). Contributed by @florianduros.
|
||||
* Improve accessibility of the `\<AvatarSetting> component ([#30907](https://github.com/element-hq/element-web/pull/30907)). Contributed by @MidhunSureshR.
|
||||
|
||||
|
||||
|
||||
Changes in [1.12.1](https://github.com/element-hq/element-desktop/releases/tag/v1.12.1) (2025-10-07)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Docker image to facilitate building Element Desktop's native bits using a glibc version (2.31)
|
||||
# with broader compatibility, down to Debian bullseye & Ubuntu focal.
|
||||
FROM rust:bullseye@sha256:8f72d971a31b278cebdb2eb64a44c3900ff27c716a4ef6f4db05946d10c9ae4e
|
||||
FROM rust:bullseye@sha256:65dd7bbc1811b11827fcc842491b90dcd5ebc7153132596911986932a59a0e12
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
set -x
|
||||
declare -A archMap=(["amd64"]="x64" ["arm64"]="arm64")
|
||||
ARCH="${archMap["$TARGETARCH"]}"
|
||||
NODE_VERSION=$(cat /.node-version)
|
||||
curl --proto "=https" -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$TARGETOS-$ARCH.tar.gz" | tar xz -C /usr/local --strip-components=1 && \
|
||||
# The .node-version file generally doesn't have the 'v' (renovate does not put the 'v' and will
|
||||
# strip it on upgrade if it's there) but the 'v' is also widely supported so we probably ought
|
||||
# to just work either way.
|
||||
NODE_VERSION=$(cat /.node-version | sed -e 's/^v//')
|
||||
curl --proto "=https" -L "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-$TARGETOS-$ARCH.tar.gz" | tar xz -C /usr/local --strip-components=1 && \
|
||||
unlink /usr/local/CHANGELOG.md && unlink /usr/local/LICENSE && unlink /usr/local/README.md
|
||||
|
||||
@@ -129,7 +129,7 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
||||
linux: {
|
||||
target: ["tar.gz", "deb"],
|
||||
category: "Network;InstantMessaging;Chat",
|
||||
icon: "build/icon.png",
|
||||
icon: "icon.png",
|
||||
executableName: variant.name, // element-desktop or element-desktop-nightly
|
||||
},
|
||||
deb: {
|
||||
@@ -151,12 +151,12 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
|
||||
fpm: ["--deb-pre-depends", "libc6 (>= 2.31)"],
|
||||
},
|
||||
mac: {
|
||||
target: ["dmg", "zip"],
|
||||
category: "public.app-category.social-networking",
|
||||
darkModeSupport: true,
|
||||
hardenedRuntime: true,
|
||||
gatekeeperAssess: true,
|
||||
// XXX: we cannot specify this due to https://github.com/electron/osx-sign/issues/344
|
||||
// strictVerify: true,
|
||||
strictVerify: true,
|
||||
entitlements: "./build/entitlements.mac.plist",
|
||||
icon: "build/icon.icns",
|
||||
mergeASARs: true,
|
||||
|
||||
4
knip.ts
4
knip.ts
@@ -1,7 +1,7 @@
|
||||
import { KnipConfig } from "knip";
|
||||
|
||||
export default {
|
||||
entry: ["src/electron-main.ts", "src/preload.ts", "electron-builder.ts", ".eslintrc-*.js", "scripts/**", "hak/**"],
|
||||
entry: ["src/preload.cts", "electron-builder.ts", "scripts/**", "hak/**"],
|
||||
project: ["**/*.{js,ts}"],
|
||||
ignoreDependencies: [
|
||||
// Brought in via hak scripts
|
||||
@@ -10,6 +10,8 @@ export default {
|
||||
"@action-validator/*",
|
||||
// Used for git pre-commit hooks
|
||||
"husky",
|
||||
// Required for `patch-package`
|
||||
"postinstall-postinstall",
|
||||
],
|
||||
ignoreBinaries: ["jq", "scripts/in-docker.sh"],
|
||||
} satisfies KnipConfig;
|
||||
|
||||
14
package.json
14
package.json
@@ -3,7 +3,7 @@
|
||||
"productName": "Element",
|
||||
"main": "lib/electron-main.js",
|
||||
"exports": "./lib/electron-main.js",
|
||||
"version": "1.12.1",
|
||||
"version": "1.12.2",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": {
|
||||
"name": "Element",
|
||||
@@ -57,7 +57,7 @@
|
||||
"test:open": "yarn test --ui",
|
||||
"test:screenshots:build": "docker build playwright -t element-desktop-playwright --platform linux/amd64",
|
||||
"test:screenshots:run": "docker run --rm --network host -v $(pwd):/work/element-desktop -v /var/run/docker.sock:/var/run/docker.sock --platform linux/amd64 -it element-desktop-playwright",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
"postinstall": "patch-package && electron-builder install-app-deps"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/electron": "^7.0.0",
|
||||
@@ -77,19 +77,19 @@
|
||||
"@babel/preset-env": "^7.18.10",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@electron/asar": "4.0.1",
|
||||
"@playwright/test": "1.55.0",
|
||||
"@playwright/test": "1.55.1",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/auto-launch": "^5.0.1",
|
||||
"@types/counterpart": "^0.18.1",
|
||||
"@types/minimist": "^1.2.1",
|
||||
"@types/node": "18.19.124",
|
||||
"@types/node": "18.19.127",
|
||||
"@types/pacote": "^11.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"app-builder-lib": "26.0.20",
|
||||
"chokidar": "^4.0.0",
|
||||
"detect-libc": "^2.0.0",
|
||||
"electron": "38.1.0",
|
||||
"electron": "38.2.0",
|
||||
"electron-builder": "26.0.20",
|
||||
"electron-builder-squirrel-windows": "26.0.20",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
@@ -107,6 +107,8 @@
|
||||
"matrix-web-i18n": "^3.2.1",
|
||||
"mkdirp": "^3.0.0",
|
||||
"pacote": "^21.0.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^3.0.0",
|
||||
"rimraf": "^6.0.0",
|
||||
"tar": "^7.0.0",
|
||||
@@ -117,7 +119,7 @@
|
||||
"matrix-seshat": "^4.0.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "18.19.124",
|
||||
"@types/node": "18.19.127",
|
||||
"config-file-ts": "0.2.8-rc1",
|
||||
"node-abi": "4.14.0"
|
||||
}
|
||||
|
||||
18
patches/@types+auto-launch+5.0.5.patch
Normal file
18
patches/@types+auto-launch+5.0.5.patch
Normal file
@@ -0,0 +1,18 @@
|
||||
diff --git a/node_modules/@types/auto-launch/index.d.ts b/node_modules/@types/auto-launch/index.d.ts
|
||||
index a30a77c..e512ce1 100644
|
||||
--- a/node_modules/@types/auto-launch/index.d.ts
|
||||
+++ b/node_modules/@types/auto-launch/index.d.ts
|
||||
@@ -25,6 +25,13 @@ interface AutoLaunchOptions {
|
||||
declare class AutoLaunch {
|
||||
constructor(options: AutoLaunchOptions);
|
||||
|
||||
+ /**
|
||||
+ * This type describes the internal options of the `auto-launch` package which allows us to update options after initialization.
|
||||
+ */
|
||||
+ readonly opts: {
|
||||
+ isHiddenOnLaunch: boolean;
|
||||
+ };
|
||||
+
|
||||
/**
|
||||
* Enables auto-launch at start up.
|
||||
*/
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/playwright:v1.55.0-jammy@sha256:55db7b712a981e9cc1ec757a4511cd01c5ed4e20bd7c84b2dee4e1d320102b9d
|
||||
FROM mcr.microsoft.com/playwright:v1.55.1-jammy@sha256:a012a91c32e36b3300514684c5bdb60c07f01464abd03380320179e5273427ab
|
||||
|
||||
WORKDIR /work/element-desktop
|
||||
|
||||
|
||||
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
@@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { type BrowserWindow } from "electron";
|
||||
|
||||
import type AutoLaunch from "auto-launch";
|
||||
import { type AppLocalization } from "../language-helper.js";
|
||||
|
||||
// global type extensions need to use var for whatever reason
|
||||
@@ -18,7 +17,6 @@ declare global {
|
||||
var mainWindow: BrowserWindow | null;
|
||||
var appQuitting: boolean;
|
||||
var appLocalization: AppLocalization;
|
||||
var launcher: AutoLaunch;
|
||||
var vectorConfig: IConfigOptions;
|
||||
var trayConfig: {
|
||||
// eslint-disable-next-line camelcase
|
||||
|
||||
50
src/auto-launch.ts
Normal file
50
src/auto-launch.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import BaseAutoLaunch from "auto-launch";
|
||||
|
||||
import Store from "./store.js";
|
||||
|
||||
export type AutoLaunchState = "enabled" | "minimised" | "disabled";
|
||||
|
||||
// Wrapper around auto-launch to get/set the `isHidden` option
|
||||
export class AutoLaunch extends BaseAutoLaunch {
|
||||
private static internalInstance?: AutoLaunch;
|
||||
|
||||
public static get instance(): AutoLaunch {
|
||||
if (!AutoLaunch.internalInstance) {
|
||||
if (!Store.instance) throw new Error("Store not initialized");
|
||||
AutoLaunch.internalInstance = new AutoLaunch({
|
||||
name: global.vectorConfig.brand || "Element",
|
||||
isHidden: Store.instance.get("openAtLoginMinimised"),
|
||||
mac: {
|
||||
useLaunchAgent: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return AutoLaunch.internalInstance;
|
||||
}
|
||||
|
||||
public async getState(): Promise<AutoLaunchState> {
|
||||
if (!(await this.isEnabled())) {
|
||||
return "disabled";
|
||||
}
|
||||
return this.opts.isHiddenOnLaunch ? "minimised" : "enabled";
|
||||
}
|
||||
|
||||
public async setState(state: AutoLaunchState): Promise<void> {
|
||||
const openAtLoginMinimised = state === "minimised";
|
||||
Store.instance?.set("openAtLoginMinimised", openAtLoginMinimised);
|
||||
this.opts.isHiddenOnLaunch = openAtLoginMinimised;
|
||||
|
||||
if (state !== "disabled") {
|
||||
return this.enable();
|
||||
} else {
|
||||
return this.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
} from "electron";
|
||||
// eslint-disable-next-line n/file-extension-in-import
|
||||
import * as Sentry from "@sentry/electron/main";
|
||||
import AutoLaunch from "auto-launch";
|
||||
import path, { dirname } from "node:path";
|
||||
import windowStateKeeper from "electron-window-state";
|
||||
import fs, { promises as afs } from "node:fs";
|
||||
@@ -169,59 +168,74 @@ function loadLocalConfigFile(): Json {
|
||||
}
|
||||
}
|
||||
|
||||
let loadConfigPromise: Promise<void> | undefined;
|
||||
// Loads the config from asar, and applies a config.json from userData atop if one exists
|
||||
// Writes config to `global.vectorConfig`. Does nothing if `global.vectorConfig` is already set.
|
||||
async function loadConfig(): Promise<void> {
|
||||
if (global.vectorConfig) return;
|
||||
// Writes config to `global.vectorConfig`. Idempotent, returns the same promise on subsequent calls.
|
||||
function loadConfig(): Promise<void> {
|
||||
if (loadConfigPromise) return loadConfigPromise;
|
||||
|
||||
const asarPath = await getAsarPath();
|
||||
async function actuallyLoadConfig(): Promise<void> {
|
||||
const asarPath = await getAsarPath();
|
||||
|
||||
try {
|
||||
console.log(`Loading app config: ${path.join(asarPath, LocalConfigFilename)}`);
|
||||
global.vectorConfig = loadJsonFile(asarPath, LocalConfigFilename);
|
||||
} catch {
|
||||
// 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 = {};
|
||||
}
|
||||
|
||||
try {
|
||||
// Load local config and use it to override values from the one baked with the build
|
||||
const localConfig = loadLocalConfigFile();
|
||||
|
||||
// If the local config has a homeserver defined, don't use the homeserver from the build
|
||||
// config. This is to avoid a problem where Riot thinks there are multiple homeservers
|
||||
// defined, and panics as a result.
|
||||
if (Object.keys(localConfig).find((k) => homeserverProps.includes(<any>k))) {
|
||||
// Rip out all the homeserver options from the vector config
|
||||
global.vectorConfig = Object.keys(global.vectorConfig)
|
||||
.filter((k) => !homeserverProps.includes(<any>k))
|
||||
.reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = global.vectorConfig[key];
|
||||
return obj;
|
||||
},
|
||||
{} as Omit<Partial<(typeof global)["vectorConfig"]>, keyof typeof homeserverProps>,
|
||||
);
|
||||
try {
|
||||
console.log(`Loading app config: ${path.join(asarPath, LocalConfigFilename)}`);
|
||||
global.vectorConfig = loadJsonFile(asarPath, LocalConfigFilename);
|
||||
} catch {
|
||||
// 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 = {};
|
||||
}
|
||||
|
||||
global.vectorConfig = Object.assign(global.vectorConfig, localConfig);
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
void 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"}.`,
|
||||
detail: e.message || "",
|
||||
try {
|
||||
// Load local config and use it to override values from the one baked with the build
|
||||
const localConfig = loadLocalConfigFile();
|
||||
|
||||
// If the local config has a homeserver defined, don't use the homeserver from the build
|
||||
// config. This is to avoid a problem where Riot thinks there are multiple homeservers
|
||||
// defined, and panics as a result.
|
||||
if (Object.keys(localConfig).find((k) => homeserverProps.includes(<any>k))) {
|
||||
// Rip out all the homeserver options from the vector config
|
||||
global.vectorConfig = Object.keys(global.vectorConfig)
|
||||
.filter((k) => !homeserverProps.includes(<any>k))
|
||||
.reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = global.vectorConfig[key];
|
||||
return obj;
|
||||
},
|
||||
{} as Omit<Partial<(typeof global)["vectorConfig"]>, keyof typeof homeserverProps>,
|
||||
);
|
||||
}
|
||||
|
||||
global.vectorConfig = Object.assign(global.vectorConfig, localConfig);
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
void 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"}.`,
|
||||
detail: e.message || "",
|
||||
});
|
||||
}
|
||||
|
||||
// Could not load local config, this is expected in most cases.
|
||||
}
|
||||
|
||||
// Tweak modules paths as they assume the root is at the same level as webapp, but for `vector://vector/webapp` it is not.
|
||||
if (Array.isArray(global.vectorConfig.modules)) {
|
||||
global.vectorConfig.modules = global.vectorConfig.modules.map((m) => {
|
||||
if (m.startsWith("/")) {
|
||||
return "/webapp" + m;
|
||||
}
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
// Could not load local config, this is expected in most cases.
|
||||
}
|
||||
loadConfigPromise = actuallyLoadConfig();
|
||||
return loadConfigPromise;
|
||||
}
|
||||
|
||||
// Configure Electron Sentry and crashReporter using sentry.dsn in config.json if one is present.
|
||||
@@ -239,7 +253,7 @@ async function configureSentry(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Set up globals for Tray and AutoLaunch
|
||||
// Set up globals for Tray
|
||||
async function setupGlobals(): Promise<void> {
|
||||
const asarPath = await getAsarPath();
|
||||
await loadConfig();
|
||||
@@ -250,15 +264,6 @@ async function setupGlobals(): Promise<void> {
|
||||
icon_path: path.join(path.dirname(asarPath), "build", iconFile),
|
||||
brand: global.vectorConfig.brand || "Element",
|
||||
};
|
||||
|
||||
// launcher
|
||||
global.launcher = new AutoLaunch({
|
||||
name: global.vectorConfig.brand || "Element",
|
||||
isHidden: true,
|
||||
mac: {
|
||||
useLaunchAgent: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
global.appQuitting = false;
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
"speech_start_speaking": "Mulai Berbicara",
|
||||
"speech_stop_speaking": "Berhenti Berbicara"
|
||||
},
|
||||
"eol": {
|
||||
"no_more_updates": "Anda menggunakan versi macOS yang tidak didukung. Harap tingkatkan untuk menerima pembaruan %(brand)s.",
|
||||
"title": "Sistem tidak didukung",
|
||||
"warning": "Anda menggunakan versi macOS yang tidak didukung. Harap perbarui untuk memastikan%(brand)s terus bekerja."
|
||||
},
|
||||
"file_menu": {
|
||||
"label": "Berkas"
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"brand_help": "%(brand)s Hjelp",
|
||||
"help": "Hjelp",
|
||||
"no": "Nei",
|
||||
"preferences": "Brukervalg",
|
||||
"preferences": "Innstillinger",
|
||||
"yes": "Ja"
|
||||
},
|
||||
"confirm_quit": "Er du sikker på at du vil slutte?",
|
||||
@@ -32,6 +32,11 @@
|
||||
"speech_start_speaking": "Begynn å snakke",
|
||||
"speech_stop_speaking": "Slutt å snakke"
|
||||
},
|
||||
"eol": {
|
||||
"no_more_updates": "Du kjører en versjon av macOS som ikke støttes. Oppgrader for å motta oppdateringer fr %(brand)s.",
|
||||
"title": "Systemet støttes ikke",
|
||||
"warning": "Du bruker en versjon av macOS som ikke støttes. Oppgrader for å sikre at %(brand)s fortsetter å fungere."
|
||||
},
|
||||
"file_menu": {
|
||||
"label": "Fil"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
"speech_start_speaking": "Zacznij mówić",
|
||||
"speech_stop_speaking": "Przestań mówić"
|
||||
},
|
||||
"eol": {
|
||||
"no_more_updates": "Korzystasz z nieobsługiwanej wersji systemu macOS. Zaktualizuj system, aby uzyskać aktualizacje %(brand)s.",
|
||||
"title": "System nie jest obsługiwany",
|
||||
"warning": "Korzystasz z nieobsługiwanej wersji systemu macOS. Zaktualizuj system, aby dalej korzystać z %(brand)s."
|
||||
},
|
||||
"file_menu": {
|
||||
"label": "Plik"
|
||||
},
|
||||
|
||||
@@ -32,9 +32,15 @@
|
||||
"speech_start_speaking": "Говорите",
|
||||
"speech_stop_speaking": "Перестаньте говорить"
|
||||
},
|
||||
"eol": {
|
||||
"title": "Система не поддерживается"
|
||||
},
|
||||
"file_menu": {
|
||||
"label": "Файл"
|
||||
},
|
||||
"icon_overlay": {
|
||||
"description_error": "Ошибка"
|
||||
},
|
||||
"menu": {
|
||||
"hide": "Скрыть",
|
||||
"hide_others": "Скрыть прочие",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ipcMain } from "electron";
|
||||
|
||||
import * as tray from "./tray.js";
|
||||
import Store from "./store.js";
|
||||
import { AutoLaunch, type AutoLaunchState } from "./auto-launch.js";
|
||||
|
||||
interface Setting {
|
||||
read(): Promise<any>;
|
||||
@@ -18,15 +19,11 @@ interface Setting {
|
||||
|
||||
const Settings: Record<string, Setting> = {
|
||||
"Electron.autoLaunch": {
|
||||
async read(): Promise<any> {
|
||||
return global.launcher.isEnabled();
|
||||
async read(): Promise<AutoLaunchState> {
|
||||
return AutoLaunch.instance.getState();
|
||||
},
|
||||
async write(value: any): Promise<void> {
|
||||
if (value) {
|
||||
return global.launcher.enable();
|
||||
} else {
|
||||
return global.launcher.disable();
|
||||
}
|
||||
async write(value: AutoLaunchState): Promise<void> {
|
||||
return AutoLaunch.instance.setState(value);
|
||||
},
|
||||
},
|
||||
"Electron.warnBeforeExit": {
|
||||
|
||||
@@ -89,6 +89,8 @@ interface StoreData {
|
||||
safeStorageBackendOverride?: boolean;
|
||||
/** whether to perform a migration of the safeStorage data */
|
||||
safeStorageBackendMigrate?: boolean;
|
||||
/** whether to open the app at login minimised, only valid when app.openAtLogin is true */
|
||||
openAtLoginMinimised: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,6 +234,10 @@ class Store extends ElectronStore<StoreData> {
|
||||
safeStorageBackendMigrate: {
|
||||
type: "boolean",
|
||||
},
|
||||
openAtLoginMinimised: {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user