Merge branch 'develop' into dbkr/key_backup_by_default

This commit is contained in:
David Baker 2024-12-02 14:44:46 +00:00 committed by GitHub
commit 6bff653339
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
477 changed files with 5526 additions and 16820 deletions

View file

@ -0,0 +1,33 @@
name: Upload release assets
description: Uploads assets to an existing release and optionally signs them
inputs:
tag:
description: GitHub release tag to fetch assets from.
required: true
out-file-path:
description: Path to where the webapp should be extracted to.
required: true
runs:
using: composite
steps:
- name: Download current version for its old bundles
id: current_download
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
with:
tag: steps.current_version.outputs.version
fileName: element-*.tar.gz*
out-file-path: ${{ runner.temp }}/download-verify-element-tarball
- name: Verify tarball
run: gpg --verify element-*.tar.gz.asc element-*.tar.gz
working-directory: ${{ runner.temp }}/download-verify-element-tarball
- name: Extract tarball
run: tar xvzf element-*.tar.gz -C webapp --strip-components=1
working-directory: ${{ runner.temp }}/download-verify-element-tarball
- name: Move webapp to out-file-path
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }}
- name: Clean up temp directory
run: rm -R ${{ runner.temp }}/download-verify-element-tarball

3
.github/labels.yml vendored
View file

@ -232,6 +232,9 @@
- name: "Z-Flaky-Test" - name: "Z-Flaky-Test"
description: "A test is raising false alarms" description: "A test is raising false alarms"
color: "ededed" color: "ededed"
- name: "Z-Flaky-Jest-Test"
description: "A Jest test is raising false alarms"
color: "ededed"
- name: "Z-FOSDEM" - name: "Z-FOSDEM"
description: "Issues in chat.fosdem.org" description: "Issues in chat.fosdem.org"
color: "ededed" color: "ededed"

View file

@ -7,6 +7,8 @@ on:
branches: branches:
- develop - develop
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
backport: backport:
name: Backport name: Backport

View file

@ -10,6 +10,7 @@ env:
# These must be set for fetchdep.sh to get the right branch # These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
permissions: {} # No permissions required
jobs: jobs:
build: build:
name: "Build on ${{ matrix.image }}" name: "Build on ${{ matrix.image }}"

View file

@ -3,6 +3,7 @@ on:
release: release:
types: [published] types: [published]
concurrency: ${{ github.workflow }} concurrency: ${{ github.workflow }}
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
build: build:
name: Build package name: Build package

View file

@ -9,6 +9,7 @@ on:
concurrency: concurrency:
group: ${{ github.repository_owner }}-${{ github.workflow }}-${{ github.ref_name }} group: ${{ github.repository_owner }}-${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
build: build:
name: "Build & Deploy develop.element.io" name: "Build & Deploy develop.element.io"
@ -16,6 +17,10 @@ jobs:
if: github.repository == 'element-hq/element-web' if: github.repository == 'element-hq/element-web'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
environment: develop environment: develop
permissions:
checks: read
pages: write
deployments: write
env: env:
R2_BUCKET: "element-web-develop" R2_BUCKET: "element-web-develop"
R2_URL: ${{ vars.CF_R2_S3_API }} R2_URL: ${{ vars.CF_R2_S3_API }}

88
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,88 @@
# Manual deploy workflow for deploying to app.element.io & staging.element.io
# Runs automatically for staging.element.io when an RC or Release is published
# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand
name: Build and Deploy ${{ inputs.site || 'staging.element.io' }}
on:
release:
types: [published]
workflow_dispatch:
inputs:
site:
description: Which site to deploy to
required: true
default: staging.element.io
type: choice
options:
- staging.element.io
- app.element.io
concurrency: ${{ inputs.site || 'staging.element.io' }}
permissions: {}
jobs:
deploy:
name: "Deploy to Cloudflare Pages"
runs-on: ubuntu-24.04
environment: ${{ inputs.site || 'staging.element.io' }}
permissions:
checks: read
deployments: write
env:
SITE: ${{ inputs.site || 'staging.element.io' }}
steps:
- name: Load GPG key
run: |
curl https://packages.element.io/element-release-key.gpg | gpg --import
gpg -k "$GPG_FINGERPRINT"
env:
GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }}
- name: Check current version on deployment
id: current_version
run: |
echo "version=$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT
# The current version bundle melding dance is skipped if the version we're deploying is the same
# as then we're just doing a re-deploy of the same version with potentially different configs.
- name: Download current version for its old bundles
id: current_download
if: steps.current_version.outputs.version != github.ref_name
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
with:
tag: steps.current_version.outputs.version
out-file-path: current_version
- name: Download target version
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
with:
tag: ${{ github.ref_name }}
out-file-path: _deploy
- name: Merge current bundles into target
if: steps.current_download.outcome == 'success'
run: cp -vnpr current_version/bundles/* _deploy/bundles/
- name: Copy config
run: cp element.io/app/config.json _deploy/config.json
- name: Populate 404.html
run: echo "404 Not Found" > _deploy/404.html
- name: Populate _headers
run: cp .github/cfp_headers _deploy/_headers
- name: Wait for other steps to succeed
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with:
ref: ${{ github.sha }}
running-workflow-name: "Build and Deploy ${{ env.SITE }}"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1
with:
apiToken: ${{ secrets.CF_PAGES_TOKEN }}
accountId: ${{ secrets.CF_PAGES_ACCOUNT_ID }}
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
directory: _deploy
gitHubToken: ${{ secrets.GITHUB_TOKEN }}

View file

@ -7,14 +7,14 @@ on:
# This job can take a while, and we have usage limits, so just publish develop only twice a day # This job can take a while, and we have usage limits, so just publish develop only twice a day
- cron: "0 7/12 * * *" - cron: "0 7/12 * * *"
concurrency: ${{ github.workflow }}-${{ github.ref_name }} concurrency: ${{ github.workflow }}-${{ github.ref_name }}
permissions: {}
permissions:
id-token: write # needed for signing the images with GitHub OIDC Token
jobs: jobs:
buildx: buildx:
name: Docker Buildx name: Docker Buildx
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
environment: dockerhub environment: dockerhub
permissions:
id-token: write # needed for signing the images with GitHub OIDC Token
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -39,7 +39,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5 uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
with: with:
images: | images: |
vectorim/element-web vectorim/element-web
@ -51,7 +51,7 @@ jobs:
- name: Build and push - name: Build and push
id: build-and-push id: build-and-push
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6 uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
with: with:
context: . context: .
push: true push: true

View file

@ -5,10 +5,7 @@ on:
branches: [develop] branches: [develop]
workflow_dispatch: {} workflow_dispatch: {}
permissions: permissions: {}
contents: read
pages: write
id-token: write
concurrency: concurrency:
group: "pages" group: "pages"
@ -100,6 +97,9 @@ jobs:
name: github-pages name: github-pages
url: ${{ steps.deployment.outputs.page_url }} url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
pages: write
id-token: write
needs: build needs: build
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages

View file

@ -11,6 +11,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }} group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }} cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
permissions: {}
jobs: jobs:
report: report:
if: github.event.workflow_run.conclusion != 'cancelled' if: github.event.workflow_run.conclusion != 'cancelled'
@ -20,11 +22,12 @@ jobs:
permissions: permissions:
statuses: write statuses: write
deployments: write deployments: write
actions: read
steps: steps:
- name: Download HTML report - name: Download HTML report
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}
name: html-report name: html-report
path: playwright-report path: playwright-report

View file

@ -33,6 +33,8 @@ env:
# fetchdep.sh needs to know our PR number # fetchdep.sh needs to know our PR number
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
permissions: {} # No permissions required
jobs: jobs:
build: build:
name: "Build Element-Web" name: "Build Element-Web"

View file

@ -4,6 +4,7 @@
on: on:
issues: issues:
types: [closed] types: [closed]
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
tidy: tidy:
name: Tidy closed issues name: Tidy closed issues

View file

@ -3,6 +3,7 @@ on:
workflow_dispatch: {} workflow_dispatch: {}
schedule: schedule:
- cron: "0 6 * * 1,3,5" # Every Monday, Wednesday and Friday at 6am UTC - cron: "0 6 * * 1,3,5" # Every Monday, Wednesday and Friday at 6am UTC
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
download: download:
uses: matrix-org/matrix-web-i18n/.github/workflows/localazy_download.yaml@main uses: matrix-org/matrix-web-i18n/.github/workflows/localazy_download.yaml@main

View file

@ -4,6 +4,7 @@ on:
branches: [develop] branches: [develop]
paths: paths:
- "src/i18n/strings/en_EN.json" - "src/i18n/strings/en_EN.json"
permissions: {} # No permissions needed
jobs: jobs:
upload: upload:
uses: matrix-org/matrix-web-i18n/.github/workflows/localazy_upload.yaml@main uses: matrix-org/matrix-web-i18n/.github/workflows/localazy_upload.yaml@main

View file

@ -11,6 +11,9 @@ jobs:
if: github.event.workflow_run.conclusion != 'cancelled' && github.event.workflow_run.event == 'pull_request' if: github.event.workflow_run.conclusion != 'cancelled' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
environment: Netlify environment: Netlify
permissions:
actions: read
deployments: write
steps: steps:
- name: 📝 Create Deployment - name: 📝 Create Deployment
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1 uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1
@ -27,7 +30,7 @@ jobs:
- name: 📥 Download artifact - name: 📥 Download artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}
name: webapp name: webapp
path: webapp path: webapp

View file

@ -6,6 +6,7 @@ on:
#schedule: #schedule:
# - cron: "*/10 * * * *" # - cron: "*/10 * * * *"
concurrency: ${{ github.workflow }} concurrency: ${{ github.workflow }}
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
bot: bot:
name: Pending reviews bot name: Pending reviews bot

View file

@ -3,9 +3,12 @@ on:
workflow_dispatch: {} workflow_dispatch: {}
schedule: schedule:
- cron: "0 6 * * *" # Every day at 6am UTC - cron: "0 6 * * *" # Every day at 6am UTC
permissions: {}
jobs: jobs:
update: update:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -4,8 +4,11 @@ on:
types: [opened, edited, labeled, unlabeled, synchronize] types: [opened, edited, labeled, unlabeled, synchronize]
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
permissions: {}
jobs: jobs:
action: action:
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
permissions:
pull-requests: write
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -2,6 +2,7 @@ name: Pull Request Base Branch
on: on:
pull_request: pull_request:
types: [opened, edited, synchronize] types: [opened, edited, synchronize]
permissions: {} # No permissions required
jobs: jobs:
check_base_branch: check_base_branch:
name: Check PR base branch name: Check PR base branch

View file

@ -4,6 +4,9 @@ on:
branches: [staging] branches: [staging]
workflow_dispatch: {} workflow_dispatch: {}
concurrency: ${{ github.workflow }} concurrency: ${{ github.workflow }}
permissions: {}
jobs: jobs:
draft: draft:
permissions:
contents: write
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop

View file

@ -4,6 +4,7 @@ on:
push: push:
branches: [master] branches: [master]
concurrency: ${{ github.repository }}-${{ github.workflow }} concurrency: ${{ github.repository }}-${{ github.workflow }}
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
merge: merge:
uses: matrix-org/matrix-js-sdk/.github/workflows/release-gitflow.yml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/release-gitflow.yml@develop

View file

@ -11,9 +11,14 @@ on:
- rc - rc
- final - final
concurrency: ${{ github.workflow }} concurrency: ${{ github.workflow }}
permissions: {}
jobs: jobs:
release: release:
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
permissions:
contents: write
issues: write
pull-requests: read
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
@ -42,6 +47,8 @@ jobs:
name: Post release checks name: Post release checks
needs: release needs: release
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
checks: read
steps: steps:
- name: Wait for dockerhub - name: Wait for dockerhub
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork

View file

@ -17,9 +17,25 @@ on:
required: true required: true
type: boolean type: boolean
default: true default: true
permissions: {} # Uses ELEMENT_BOT_TOKEN instead
jobs: jobs:
checks:
name: Sanity checks
strategy:
matrix:
repo:
- matrix-org/matrix-js-sdk
- element-hq/element-web
- element-hq/element-desktop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
with:
repository: ${{ matrix.repo }}
prepare: prepare:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: checks
env: env:
# The order is specified bottom-up to avoid any races for allchange # The order is specified bottom-up to avoid any races for allchange
REPOS: matrix-js-sdk element-web element-desktop REPOS: matrix-js-sdk element-web element-desktop

View file

@ -7,11 +7,16 @@ on:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
sonarqube: sonarqube:
name: 🩻 SonarQube name: 🩻 SonarQube
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group' if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
permissions:
actions: read
statuses: write
id-token: write # sonar
secrets: secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -16,6 +16,8 @@ env:
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
permissions: {} # No permissions required
jobs: jobs:
ts_lint: ts_lint:
name: "Typescript Syntax Check" name: "Typescript Syntax Check"
@ -37,6 +39,8 @@ jobs:
i18n_lint: i18n_lint:
name: "i18n Check" name: "i18n Check"
uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main
permissions:
pull-requests: read
with: with:
hardcoded-words: "Element" hardcoded-words: "Element"
allowed-hardcoded-keys: | allowed-hardcoded-keys: |

View file

@ -8,6 +8,9 @@ on:
- develop - develop
paths: paths:
- .github/labels.yml - .github/labels.yml
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
sync-labels: sync-labels:
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop

View file

@ -26,6 +26,8 @@ env:
# fetchdep.sh needs to know our PR number # fetchdep.sh needs to know our PR number
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
permissions: {}
jobs: jobs:
jest: jest:
name: Jest name: Jest
@ -94,13 +96,15 @@ jobs:
needs: jest needs: jest
if: always() if: always()
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
statuses: write
steps: steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success' - if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
run: exit 1 run: exit 1
- name: Skip SonarCloud in merge queue - name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1 uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: success state: success

View file

@ -4,6 +4,8 @@ on:
issues: issues:
types: [assigned] types: [assigned]
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
web-app-team: web-app-team:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04

View file

@ -4,6 +4,8 @@ on:
issues: issues:
types: [opened] types: [opened]
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
automate-project-columns: automate-project-columns:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04

View file

@ -8,6 +8,8 @@ on:
ELEMENT_BOT_TOKEN: ELEMENT_BOT_TOKEN:
required: true required: true
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
apply_Z-Labs_label: apply_Z-Labs_label:
name: Add Z-Labs label for features behind labs flags name: Add Z-Labs label for features behind labs flags

View file

@ -3,6 +3,7 @@ on:
pull_request_target: pull_request_target:
types: [review_requested] types: [review_requested]
permissions: {} # Uses ELEMENT_BOT_TOKEN instead
jobs: jobs:
add_design_pr_to_project: add_design_pr_to_project:
name: Move PRs asking for design review to the design board name: Move PRs asking for design review to the design board

View file

@ -2,6 +2,7 @@ name: Close stale flaky issues
on: on:
schedule: schedule:
- cron: "30 1 * * *" - cron: "30 1 * * *"
permissions: {}
jobs: jobs:
close: close:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04

View file

@ -3,11 +3,13 @@ name: Move unlabelled from needs info columns to triaged
on: on:
issues: issues:
types: [unlabeled] types: [unlabeled]
permissions: {}
jobs: jobs:
Move_Unabeled_Issue_On_Project_Board: Move_Unabeled_Issue_On_Project_Board:
name: Move no longer X-Needs-Info issues to Triaged name: Move no longer X-Needs-Info issues to Triaged
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
repository-projects: read
if: > if: >
${{ ${{
!contains(github.event.issue.labels.*.name, 'X-Needs-Info') }} !contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}

View file

@ -4,6 +4,7 @@ on:
workflow_dispatch: {} workflow_dispatch: {}
schedule: schedule:
- cron: "0 3 * * 0" # 3am every Sunday - cron: "0 3 * * 0" # 3am every Sunday
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
update: update:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04

View file

@ -15,6 +15,7 @@ on:
required: true required: true
type: string type: string
concurrency: ${{ github.workflow }} concurrency: ${{ github.workflow }}
permissions: {} # No permissions required
jobs: jobs:
bot: bot:
name: Release topic update name: Release topic update

View file

@ -1,3 +1,23 @@
Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19)
==================================================================================================
## ✨ Features
* Deduplicate icons using Compound Design Tokens ([#28419](https://github.com/element-hq/element-web/pull/28419)). Contributed by @t3chguy.
* Let widget driver send error details ([#28357](https://github.com/element-hq/element-web/pull/28357)). Contributed by @AndrewFerr.
* Deduplicate icons using Compound Design Tokens ([#28381](https://github.com/element-hq/element-web/pull/28381)). Contributed by @t3chguy.
* Auto approvoce `io.element.call.reaction` capability for element call widgets ([#28401](https://github.com/element-hq/element-web/pull/28401)). Contributed by @toger5.
* Show message type prefix in thread root \& reply previews ([#28361](https://github.com/element-hq/element-web/pull/28361)). Contributed by @t3chguy.
* Support sending encrypted to device messages from widgets ([#28315](https://github.com/element-hq/element-web/pull/28315)). Contributed by @hughns.
## 🐛 Bug Fixes
* Feed events to widgets as they are decrypted (even if out of order) ([#28376](https://github.com/element-hq/element-web/pull/28376)). Contributed by @robintown.
* Handle authenticated media when downloading from ImageView ([#28379](https://github.com/element-hq/element-web/pull/28379)). Contributed by @t3chguy.
* Ignore `m.3pid_changes` for Identity service 3PID changes ([#28375](https://github.com/element-hq/element-web/pull/28375)). Contributed by @t3chguy.
* Fix markdown escaping wrongly passing html through ([#28363](https://github.com/element-hq/element-web/pull/28363)). Contributed by @t3chguy.
* Remove "Upgrade your encryption" flow in `CreateSecretStorageDialog` ([#28290](https://github.com/element-hq/element-web/pull/28290)). Contributed by @florianduros.
Changes in [1.11.85](https://github.com/element-hq/element-web/releases/tag/v1.11.85) (2024-11-12) Changes in [1.11.85](https://github.com/element-hq/element-web/releases/tag/v1.11.85) (2024-11-12)
================================================================================================== ==================================================================================================
# Security # Security

View file

@ -1,6 +0,0 @@
// Stub out FontManager for tests as it doesn't validate anything we don't already know given
// our fixed test environment and it requires the installation of node-canvas.
module.exports = {
fixupColorFonts: () => Promise.resolve(),
};

View file

@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only.
2. `sync_timeline_limit` 2. `sync_timeline_limit`
3. `dangerously_allow_unsafe_and_insecure_passwords` 3. `dangerously_allow_unsafe_and_insecure_passwords`
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled. 4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
5. `voice_broadcast.chunk_length`: Target chunk length in seconds for the Voice Broadcast feature currently under development.

View file

@ -32,7 +32,6 @@ const config: Config = {
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js", "decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js", "waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"context-filter-polyfill": "<rootDir>/__mocks__/empty.js", "context-filter-polyfill": "<rootDir>/__mocks__/empty.js",
"FontManager.ts": "<rootDir>/__mocks__/FontManager.js",
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js", "workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
"^!!raw-loader!.*": "jest-raw-loader", "^!!raw-loader!.*": "jest-raw-loader",
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js", "recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",

View file

@ -1,6 +1,6 @@
{ {
"name": "element-web", "name": "element-web",
"version": "1.11.85", "version": "1.11.86",
"description": "A feature-rich client for Matrix.org", "description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.", "author": "New Vector Ltd.",
"repository": { "repository": {
@ -73,7 +73,7 @@
"resolutions": { "resolutions": {
"oidc-client-ts": "3.1.0", "oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001679", "caniuse-lite": "1.0.30001684",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0" "wrap-ansi": "npm:wrap-ansi@^7.0.0"
}, },
@ -86,7 +86,7 @@
"@matrix-org/spec": "^1.7.0", "@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0", "@sentry/browser": "^8.0.0",
"@vector-im/compound-design-tokens": "^2.0.1", "@vector-im/compound-design-tokens": "^2.0.1",
"@vector-im/compound-web": "^7.3.0", "@vector-im/compound-web": "^7.4.0",
"@vector-im/matrix-wysiwyg": "2.37.13", "@vector-im/matrix-wysiwyg": "2.37.13",
"@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4",
@ -114,10 +114,10 @@
"jsrsasign": "^11.0.0", "jsrsasign": "^11.0.0",
"jszip": "^3.7.0", "jszip": "^3.7.0",
"katex": "^0.16.0", "katex": "^0.16.0",
"linkify-element": "4.1.3", "linkify-element": "4.1.4",
"linkify-react": "4.1.3", "linkify-react": "4.1.4",
"linkify-string": "4.1.3", "linkify-string": "4.1.4",
"linkifyjs": "4.1.3", "linkifyjs": "4.1.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"maplibre-gl": "^4.0.0", "maplibre-gl": "^4.0.0",
"matrix-encrypt-attachment": "^1.0.3", "matrix-encrypt-attachment": "^1.0.3",
@ -268,11 +268,12 @@
"postcss-preset-env": "^10.0.0", "postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4", "postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prettier": "3.3.3", "prettier": "3.4.1",
"process": "^0.11.10", "process": "^0.11.10",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"rimraf": "^6.0.0", "rimraf": "^6.0.0",
"semver": "^7.5.2", "semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"stylelint": "^16.1.0", "stylelint": "^16.1.0",
"stylelint-config-standard": "^36.0.0", "stylelint-config-standard": "^36.0.0",
"stylelint-scss": "^6.0.0", "stylelint-scss": "^6.0.0",

View file

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.48.2-jammy FROM mcr.microsoft.com/playwright:v1.49.0-jammy
WORKDIR /work WORKDIR /work

View file

@ -67,6 +67,9 @@ test.describe("Cryptography", function () {
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
await app.viewRoomByName("Test room"); await app.viewRoomByName("Test room");
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
await page.waitForTimeout(1000);
// There should be two historical events in the timeline // There should be two historical events in the timeline
const tiles = await page.locator(".mx_EventTile").all(); const tiles = await page.locator(".mx_EventTile").all();
expect(tiles.length).toBeGreaterThanOrEqual(2); expect(tiles.length).toBeGreaterThanOrEqual(2);

View file

@ -16,6 +16,7 @@ import {
logOutOfElement, logOutOfElement,
verify, verify,
} from "./utils"; } from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
test.describe("Cryptography", function () { test.describe("Cryptography", function () {
test.use({ test.use({
@ -307,5 +308,30 @@ test.describe("Cryptography", function () {
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
}); });
test("should show correct shields on events sent by users with changed identity", async ({
page,
app,
bot: bob,
homeserver,
}) => {
// Verify Bob
await verify(app, bob);
// Bob logs in a new device and resets cross-signing
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
/* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("test encrypted from user that was previously verified");
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Sender's verified identity has changed",
);
});
}); });
}); });

View file

@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image. // Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI. // We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically. // This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:d947f40999b060ad4856c0af741b8619fa131430a29922606e374fdba532082b"; const DOCKER_TAG = "develop@sha256:489fe921e03440af87e001106c41c70ffc55a1e8078d1a7f45e16fbaddc5088a";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 KiB

After

Width:  |  Height:  |  Size: 975 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -319,6 +319,7 @@
@import "./views/rooms/_ThirdPartyMemberInfo.pcss"; @import "./views/rooms/_ThirdPartyMemberInfo.pcss";
@import "./views/rooms/_ThreadSummary.pcss"; @import "./views/rooms/_ThreadSummary.pcss";
@import "./views/rooms/_TopUnreadMessagesBar.pcss"; @import "./views/rooms/_TopUnreadMessagesBar.pcss";
@import "./views/rooms/_UserIdentityWarning.pcss";
@import "./views/rooms/_VoiceRecordComposerTile.pcss"; @import "./views/rooms/_VoiceRecordComposerTile.pcss";
@import "./views/rooms/_WhoIsTypingTile.pcss"; @import "./views/rooms/_WhoIsTypingTile.pcss";
@import "./views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss"; @import "./views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss";
@ -392,9 +393,3 @@
@import "./views/voip/_LegacyCallViewHeader.pcss"; @import "./views/voip/_LegacyCallViewHeader.pcss";
@import "./views/voip/_LegacyCallViewSidebar.pcss"; @import "./views/voip/_LegacyCallViewSidebar.pcss";
@import "./views/voip/_VideoFeed.pcss"; @import "./views/voip/_VideoFeed.pcss";
@import "./voice-broadcast/atoms/_LiveBadge.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss";
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";

View file

@ -22,20 +22,6 @@ Please see LICENSE files in the repository root for full details.
pointer-events: none; /* makes the avatar non-draggable */ pointer-events: none; /* makes the avatar non-draggable */
} }
} }
.mx_UserMenu_userAvatarLive {
align-items: center;
background-color: $alert;
border-radius: 6px;
color: $live-badge-color;
display: flex;
height: 12px;
justify-content: center;
left: 25px;
position: absolute;
top: 20px;
width: 12px;
}
} }
.mx_UserMenu_contextMenuButton { .mx_UserMenu_contextMenuButton {

View file

@ -256,10 +256,6 @@ Please see LICENSE files in the repository root for full details.
mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg");
} }
.mx_MessageComposer_voiceBroadcast::before {
mask-image: url("$(res)/img/element-icons/live.svg");
}
.mx_MessageComposer_plain_text::before { .mx_MessageComposer_plain_text::before {
mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg"); mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg");
} }

View file

@ -0,0 +1,28 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_UserIdentityWarning {
/* 42px is the padding-left of .mx_MessageComposer_wrapper in res/css/views/rooms/_MessageComposer.pcss */
margin-left: calc(-42px + var(--RoomView_MessageList-padding));
.mx_UserIdentityWarning_row {
display: flex;
align-items: center;
.mx_BaseAvatar {
margin-left: var(--cpd-space-2x);
}
.mx_UserIdentityWarning_main {
margin-left: var(--cpd-space-6x);
flex-grow: 1;
}
}
}
.mx_MessageComposer.mx_MessageComposer--compact > .mx_UserIdentityWarning {
margin-left: calc(-25px + var(--RoomView_MessageList-padding));
}

View file

@ -1,23 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_LiveBadge {
align-items: center;
background-color: $alert;
border-radius: 2px;
color: $live-badge-color;
display: inline-flex;
font-size: $font-12px;
font-weight: var(--cpd-font-weight-semibold);
gap: $spacing-4;
padding: 2px 4px;
}
.mx_LiveBadge--grey {
background-color: $quaternary-content;
}

View file

@ -1,28 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastControl {
align-items: center;
background-color: $background;
border-radius: 50%;
color: $secondary-content;
display: flex;
flex: 0 0 32px;
height: 32px;
justify-content: center;
width: 32px;
}
.mx_VoiceBroadcastControl-recording {
color: $alert;
}
.mx_VoiceBroadcastControl-play .mx_Icon {
left: 1px;
position: relative;
}

View file

@ -1,60 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastHeader {
align-items: flex-start;
display: flex;
gap: $spacing-8;
line-height: 20px;
margin-bottom: $spacing-16;
min-width: 0;
}
.mx_VoiceBroadcastHeader_content {
flex-grow: 1;
min-width: 0;
}
.mx_VoiceBroadcastHeader_room_wrapper {
align-items: center;
display: flex;
gap: 4px;
justify-content: flex-start;
}
.mx_VoiceBroadcastHeader_room {
font-size: $font-12px;
font-weight: var(--cpd-font-weight-semibold);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mx_VoiceBroadcastHeader_line {
align-items: center;
color: $secondary-content;
font-size: $font-12px;
display: flex;
gap: $spacing-4;
.mx_Spinner {
flex: 0 0 14px;
padding: 1px;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.mx_VoiceBroadcastHeader_mic--clickable {
cursor: pointer;
}

View file

@ -1,18 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastRecordingConnectionError {
align-items: center;
color: $alert;
display: flex;
gap: $spacing-12;
svg path {
fill: $alert;
}
}

View file

@ -1,14 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_RoomTile .mx_RoomTile_titleContainer .mx_RoomTile_subtitle.mx_RoomTile_subtitle--voice-broadcast {
align-items: center;
color: $alert;
display: flex;
gap: $spacing-4;
}

View file

@ -1,75 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastBody {
background-color: $quinary-content;
border-radius: 8px;
color: $secondary-content;
display: inline-block;
font-size: $font-12px;
padding: $spacing-12;
width: 271px;
.mx_Clock {
line-height: 1;
}
}
.mx_VoiceBroadcastBody--pip {
background-color: $system;
box-shadow: 0 2px 8px 0 #0000004a;
}
.mx_VoiceBroadcastBody--small {
display: flex;
gap: $spacing-8;
width: 192px;
.mx_VoiceBroadcastHeader {
margin-bottom: 0;
}
.mx_VoiceBroadcastControl {
align-self: center;
}
.mx_LiveBadge {
margin-top: 4px;
}
}
.mx_VoiceBroadcastBody_divider {
background-color: $quinary-content;
border: 0;
height: 1px;
margin: $spacing-12 0;
}
.mx_VoiceBroadcastBody_controls {
align-items: center;
display: flex;
gap: $spacing-32;
justify-content: center;
margin-bottom: $spacing-8;
}
.mx_VoiceBroadcastBody_timerow {
display: flex;
justify-content: space-between;
}
.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton {
display: flex;
gap: $spacing-8;
}
.mx_VoiceBroadcastBody__small-close {
right: 8px;
position: absolute;
top: 8px;
}

View file

@ -240,11 +240,6 @@ $location-live-secondary-color: #deddfd;
} }
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
/* One-off colors */ /* One-off colors */
/* ******************** */ /* ******************** */
$progressbar-bg-color: var(--cpd-color-gray-200); $progressbar-bg-color: var(--cpd-color-gray-200);

View file

@ -226,11 +226,6 @@ $location-live-color: #5c56f5;
$location-live-secondary-color: #deddfd; $location-live-secondary-color: #deddfd;
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
body { body {
color-scheme: dark; color-scheme: dark;
} }

View file

@ -325,11 +325,6 @@ $location-live-color: #5c56f5;
$location-live-secondary-color: #deddfd; $location-live-secondary-color: #deddfd;
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
body { body {
color-scheme: light; color-scheme: light;
} }

View file

@ -143,3 +143,21 @@ $inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f, U+25c2
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
/* Twemoji COLR */
@font-face {
font-family: "Twemoji";
font-weight: 400;
src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2");
}
/* For at least Chrome on Windows 10, we have to explictly add extra weights for the emoji to appear in bold messages, etc. */
@font-face {
font-family: "Twemoji";
font-weight: 600;
src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2");
}
@font-face {
font-family: "Twemoji";
font-weight: 700;
src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2");
}

View file

@ -10,8 +10,8 @@
/* Noto Color Emoji contains digits, in fixed-width, therefore causing /* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out. digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */ TODO: Consider putting all emoji fonts to the end rather than the front. */
$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif, $font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica",
"Noto Color Emoji"; sans-serif, "Noto Color Emoji";
$monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", $monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier",
monospace, "Noto Color Emoji"; monospace, "Noto Color Emoji";
@ -355,11 +355,6 @@ $location-live-color: var(--cpd-color-purple-900);
$location-live-secondary-color: var(--cpd-color-purple-600); $location-live-secondary-color: var(--cpd-color-purple-600);
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: var(--cpd-color-icon-on-solid-primary);
/* ******************** */
body { body {
color-scheme: light; color-scheme: light;
} }

View file

@ -1,47 +0,0 @@
#!/usr/bin/env python
import json
import sys
import os
if len(sys.argv) < 3:
print "Usage: %s <source> <dest>" % (sys.argv[0],)
print "eg. %s pt_BR.json pt.json" % (sys.argv[0],)
print
print "Adds any translations to <dest> that exist in <source> but not <dest>"
sys.exit(1)
srcpath = sys.argv[1]
dstpath = sys.argv[2]
tmppath = dstpath + ".tmp"
with open(srcpath) as f:
src = json.load(f)
with open(dstpath) as f:
dst = json.load(f)
toAdd = {}
for k,v in src.iteritems():
if k not in dst:
print "Adding %s" % (k,)
toAdd[k] = v
# don't just json.dumps as we'll probably re-order all the keys (and they're
# not in any given order so we can't just sort_keys). Append them to the end.
with open(dstpath) as ifp:
with open(tmppath, 'w') as ofp:
for line in ifp:
strippedline = line.strip()
if strippedline in ('{', '}'):
ofp.write(line)
elif strippedline.endswith(','):
ofp.write(line)
else:
ofp.write(' '+strippedline+',')
toAddStr = json.dumps(toAdd, indent=4, separators=(',', ': '), ensure_ascii=False, encoding="utf8").strip("{}\n")
ofp.write("\n")
ofp.write(toAddStr.encode('utf8'))
ofp.write("\n")
os.rename(tmppath, dstpath)

View file

@ -1,84 +0,0 @@
#!/usr/bin/env bash
# Fetches the js-sdk dependency for development or testing purposes
# If there exists a branch of that dependency with the same name as
# the branch the current checkout is on, use that branch. Otherwise,
# use develop.
set -x
GIT_CLONE_ARGS=("$@")
[ -z "$defbranch" ] && defbranch="develop"
# clone a specific branch of a github repo
function clone() {
org=$1
repo=$2
branch=$3
# Chop 'origin' off the start as jenkins ends up using
# branches on the origin, but this doesn't work if we
# specify the branch when cloning.
branch=${branch#origin/}
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 \
"${GIT_CLONE_ARGS[@]}"
return $?
fi
return 1
}
function dodep() {
deforg=$1
defrepo=$2
rm -rf $defrepo
# Try the PR author's branch in case it exists on the deps as well.
# Try the target branch of the push or PR.
# Use the default branch as the last resort.
if [[ "$BUILDKITE" == true ]]; then
# If BUILDKITE_BRANCH is set, it will contain either:
# * "branch" when the author's branch and target branch are in the same repo
# * "author:branch" when the author's branch is in their fork
# We can split on `:` into an array to check.
BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ })
if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
prAuthor=${BUILDKITE_BRANCH_ARRAY[0]}
prBranch=${BUILDKITE_BRANCH_ARRAY[1]}
else
prAuthor=$deforg
prBranch=$BUILDKITE_BRANCH
fi
clone $prAuthor $defrepo $prBranch ||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH ||
clone $deforg $defrepo $defbranch ||
return $?
else
clone $deforg $defrepo $ghprbSourceBranch ||
clone $deforg $defrepo $GIT_BRANCH ||
clone $deforg $defrepo `git rev-parse --abbrev-ref HEAD` ||
clone $deforg $defrepo $defbranch ||
return $?
fi
echo "$defrepo set to branch "`git -C "$defrepo" rev-parse --abbrev-ref HEAD`
}
##############################
echo 'Setting up matrix-js-sdk'
dodep matrix-org matrix-js-sdk
pushd matrix-js-sdk
yarn link
yarn install --frozen-lockfile
popd
yarn link matrix-js-sdk
##############################

View file

@ -1,64 +0,0 @@
# Copyright 2017-2024 New Vector Ltd.
# SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
# Please see LICENSE in the repository root for full details.
# genflags.sh - Generates pngs for use with CountryDropdown.js
#
# Dependencies:
# - imagemagick --with-rsvg (because default imagemagick SVG
# renderer does not produce accurate results)
#
# on macOS, this is most easily done with:
# brew install imagemagick --with-librsvg
#
# This will clone the googlei18n flag repo before converting
# all phonenumber.js-supported country flags (as SVGs) into
# PNGs that can be used by CountryDropdown.js.
set -e
# Allow CTRL+C to terminate the script
trap "echo Exited!; exit;" SIGINT SIGTERM
# git clone the google repo to get flag SVGs
git clone git@github.com:googlei18n/region-flags
for f in region-flags/svg/*.svg; do
# Skip state flags
if [[ $f =~ [A-Z]{2}-[A-Z]{2,3}.svg ]] ; then
echo "Skipping state flag "$f
continue
fi
# Skip countries not included in phonenumber.js
if [[ $f =~ (AC|CP|DG|EA|EU|IC|TA|UM|UN|XK).svg ]] ; then
echo "Skipping non-phonenumber supported flag "$f
continue
fi
# Run imagemagick convert
# -background none : transparent background
# -resize 50x30 : resize the flag to have a height of 15px (2x)
# By default, aspect ratio is respected so the width will
# be correct and not necessarily 25px.
# -filter Lanczos : use sharper resampling to avoid muddiness
# -gravity Center : keep the image central when adding an -extent
# -border 1 : add a 1px border around the flag
# -bordercolor : set the border colour
# -extent 54x54 : surround the image with padding so that it
# has the dimensions 27x27px (2x).
convert $f -background none -filter Lanczos -resize 50x30 \
-gravity Center -border 1 -bordercolor \#e0e0e0 \
-extent 54x54 $f.png
# $f.png will be region-flags/svg/XX.svg.png at this point
# Extract filename from path $f
newname=${f##*/}
# Replace .svg with .png
newname=${newname%.svg}.png
# Move the file to flags directory
mv $f.png ../res/flags/$newname
echo "Generated res/flags/"$newname
done

View file

@ -10,7 +10,6 @@ import type { IWidget } from "matrix-widget-api";
import type { BLURHASH_FIELD } from "../utils/image-media"; import type { BLURHASH_FIELD } from "../utils/image-media";
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types"; import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types"; import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types";
import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types";
import type { EncryptedFile } from "matrix-js-sdk/src/types"; import type { EncryptedFile } from "matrix-js-sdk/src/types";
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
@ -22,6 +21,13 @@ declare module "matrix-js-sdk/src/types" {
[BLURHASH_FIELD]?: string; [BLURHASH_FIELD]?: string;
} }
export interface ImageInfo {
/**
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/4230
*/
"org.matrix.msc4230.is_animated"?: boolean;
}
export interface StateEvents { export interface StateEvents {
// Jitsi-backed video room state events // Jitsi-backed video room state events
[JitsiCallMemberEventType]: JitsiCallMemberContent; [JitsiCallMemberEventType]: JitsiCallMemberContent;
@ -30,9 +36,6 @@ declare module "matrix-js-sdk/src/types" {
"im.vector.modular.widgets": IWidget | {}; "im.vector.modular.widgets": IWidget | {};
[WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent; [WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent;
// Unstable voice broadcast state events
[VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent;
// Element custom state events // Element custom state events
"im.vector.web.settings": Record<string, any>; "im.vector.web.settings": Record<string, any>;
"org.matrix.room.preview_urls": { disable: boolean }; "org.matrix.room.preview_urls": { disable: boolean };
@ -71,7 +74,5 @@ declare module "matrix-js-sdk/src/types" {
waveform?: number[]; waveform?: number[];
}; };
"org.matrix.msc3245.voice"?: {}; "org.matrix.msc3245.voice"?: {};
"io.element.voice_broadcast_chunk"?: { sequence: number };
} }
} }

View file

@ -56,6 +56,7 @@ import { createThumbnail } from "./utils/image-media";
import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer"; import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer";
import { doMaybeLocalRoomAction } from "./utils/local-room"; import { doMaybeLocalRoomAction } from "./utils/local-room";
import { SdkContextClass } from "./contexts/SDKContext"; import { SdkContextClass } from "./contexts/SDKContext";
import { blobIsAnimated } from "./utils/Image.ts";
// scraped out of a macOS hidpi (5660ppm) screenshot png // scraped out of a macOS hidpi (5660ppm) screenshot png
// 5669 px (x-axis) , 5669 px (y-axis) , per metre // 5669 px (x-axis) , 5669 px (y-axis) , per metre
@ -150,15 +151,20 @@ async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imag
thumbnailType = "image/jpeg"; thumbnailType = "image/jpeg";
} }
// We don't await this immediately so it can happen in the background
const isAnimatedPromise = blobIsAnimated(imageFile.type, imageFile);
const imageElement = await loadImageElement(imageFile); const imageElement = await loadImageElement(imageFile);
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType); const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
const imageInfo = result.info; const imageInfo = result.info;
imageInfo["org.matrix.msc4230.is_animated"] = await isAnimatedPromise;
// For lesser supported image types, always include the thumbnail even if it is larger // For lesser supported image types, always include the thumbnail even if it is larger
if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) { if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) {
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from. // we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size; const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size!;
if ( if (
// image is small enough already // image is small enough already
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL ||

View file

@ -230,12 +230,15 @@ export default class DeviceListener {
private async getKeyBackupInfo(): Promise<KeyBackupInfo | null> { private async getKeyBackupInfo(): Promise<KeyBackupInfo | null> {
if (!this.client) return null; if (!this.client) return null;
const now = new Date().getTime(); const now = new Date().getTime();
const crypto = this.client.getCrypto();
if (!crypto) return null;
if ( if (
!this.keyBackupInfo || !this.keyBackupInfo ||
!this.keyBackupFetchedAt || !this.keyBackupFetchedAt ||
this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL
) { ) {
this.keyBackupInfo = await this.client.getKeyBackupVersion(); this.keyBackupInfo = await crypto.getKeyBackupInfo();
this.keyBackupFetchedAt = now; this.keyBackupFetchedAt = now;
} }
return this.keyBackupInfo; return this.keyBackupInfo;

View file

@ -175,13 +175,6 @@ export interface IConfigOptions {
sync_timeline_limit?: number; sync_timeline_limit?: number;
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option
voice_broadcast?: {
// length per voice chunk in seconds
chunk_length?: number;
// max voice broadcast length in seconds
max_length?: number;
};
user_notice?: { user_notice?: {
title: string; title: string;
description: string; description: string;

View file

@ -55,8 +55,6 @@ import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogP
import { findDMForUser } from "./utils/dm/findDMForUser"; import { findDMForUser } from "./utils/dm/findDMForUser";
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
import { localNotificationsAreSilenced } from "./utils/notifications"; import { localNotificationsAreSilenced } from "./utils/notifications";
import { SdkContextClass } from "./contexts/SDKContext";
import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog";
import { isNotNull } from "./Typeguards"; import { isNotNull } from "./Typeguards";
import { BackgroundAudio } from "./audio/BackgroundAudio"; import { BackgroundAudio } from "./audio/BackgroundAudio";
import { Jitsi } from "./widgets/Jitsi.ts"; import { Jitsi } from "./widgets/Jitsi.ts";
@ -859,15 +857,6 @@ export default class LegacyCallHandler extends EventEmitter {
return; return;
} }
// Pause current broadcast, if any
SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause();
if (SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()) {
// Do not start a call, if recording a broadcast
showCantStartACallDialog();
return;
}
// We might be using managed hybrid widgets // We might be using managed hybrid widgets
if (isManagedHybridWidgetEnabled(room)) { if (isManagedHybridWidgetEnabled(room)) {
await addManagedHybridWidget(room); await addManagedHybridWidget(room);

View file

@ -35,13 +35,11 @@ import IdentityAuthClient from "./IdentityAuthClient";
import { crossSigningCallbacks } from "./SecurityManager"; import { crossSigningCallbacks } from "./SecurityManager";
import { SlidingSyncManager } from "./SlidingSyncManager"; import { SlidingSyncManager } from "./SlidingSyncManager";
import { _t, UserFriendlyError } from "./languageHandler"; import { _t, UserFriendlyError } from "./languageHandler";
import { SettingLevel } from "./settings/SettingLevel";
import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController"; import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import PlatformPeg from "./PlatformPeg"; import PlatformPeg from "./PlatformPeg";
import { formatList } from "./utils/FormattingUtils"; import { formatList } from "./utils/FormattingUtils";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import { Features } from "./settings/Settings";
import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts";
export interface IMatrixClientCreds { export interface IMatrixClientCreds {
@ -333,11 +331,6 @@ class MatrixClientPegClass implements IMatrixClientPeg {
logger.error("Warning! Not using an encryption key for rust crypto store."); logger.error("Warning! Not using an encryption key for rust crypto store.");
} }
// Record the fact that we used the Rust crypto stack with this client. This just guards against people
// rolling back to versions of EW that did not default to Rust crypto (which would lead to an error, since
// we cannot migrate from Rust to Legacy crypto).
await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, true);
await this.matrixClient.initRustCrypto({ await this.matrixClient.initRustCrypto({
storageKey: rustCryptoStoreKey, storageKey: rustCryptoStoreKey,
storagePassword: rustCryptoStorePassword, storagePassword: rustCryptoStorePassword,

View file

@ -49,8 +49,6 @@ import { SdkContextClass } from "./contexts/SDKContext";
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications"; import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
import ToastStore from "./stores/ToastStore"; import ToastStore from "./stores/ToastStore";
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
import { getSenderName } from "./utils/event/getSenderName";
import { stripPlainReply } from "./utils/Reply"; import { stripPlainReply } from "./utils/Reply";
import { BackgroundAudio } from "./audio/BackgroundAudio"; import { BackgroundAudio } from "./audio/BackgroundAudio";
@ -81,17 +79,6 @@ const msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null> = {
return TextForEvent.textForLocationEvent(event)(); return TextForEvent.textForLocationEvent(event)();
}, },
[MsgType.Audio]: (event: MatrixEvent): string | null => { [MsgType.Audio]: (event: MatrixEvent): string | null => {
if (event.getContent()?.[VoiceBroadcastChunkEventType]) {
if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) {
// Show a notification for the first broadcast chunk.
// At this point a user received something to listen to.
return _t("notifier|io.element.voice_broadcast_chunk", { senderName: getSenderName(event) });
}
// Mute other broadcast chunks
return null;
}
return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet()); return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet());
}, },
}; };
@ -460,8 +447,6 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
// XXX: exported for tests // XXX: exported for tests
public evaluateEvent(ev: MatrixEvent): void { public evaluateEvent(ev: MatrixEvent): void {
// Mute notifications for broadcast info events
if (ev.getType() === VoiceBroadcastInfoEventType) return;
let roomId = ev.getRoomId()!; let roomId = ev.getRoomId()!;
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) { if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
// Attempt to translate a virtual room to a native one // Attempt to translate a virtual room to a native one

View file

@ -514,7 +514,7 @@ function getWidgets(event: MessageEvent<any>, roomId: string | null): void {
sendResponse(event, widgetStateEvents); sendResponse(event, widgetStateEvents);
} }
function getRoomEncState(event: MessageEvent<any>, roomId: string): void { async function getRoomEncState(event: MessageEvent<any>, roomId: string): Promise<void> {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, _t("widget|error_need_to_be_logged_in")); sendError(event, _t("widget|error_need_to_be_logged_in"));
@ -525,7 +525,7 @@ function getRoomEncState(event: MessageEvent<any>, roomId: string): void {
sendError(event, _t("scalar|error_room_unknown")); sendError(event, _t("scalar|error_room_unknown"));
return; return;
} }
const roomIsEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId); const roomIsEncrypted = Boolean(await client.getCrypto()?.isEncryptionEnabledInRoom(roomId));
sendResponse(event, roomIsEncrypted); sendResponse(event, roomIsEncrypted);
} }

View file

@ -46,10 +46,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
logo: require("../res/img/element-desktop-logo.svg").default, logo: require("../res/img/element-desktop-logo.svg").default,
url: "https://element.io/get-started", url: "https://element.io/get-started",
}, },
voice_broadcast: {
chunk_length: 2 * 60, // two minutes
max_length: 4 * 60 * 60, // four hours
},
feedback: { feedback: {
existing_issues_url: existing_issues_url:

View file

@ -36,7 +36,6 @@ import AccessibleButton from "./components/views/elements/AccessibleButton";
import RightPanelStore from "./stores/right-panel/RightPanelStore"; import RightPanelStore from "./stores/right-panel/RightPanelStore";
import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
import { ElementCall } from "./models/Call"; import { ElementCall } from "./models/Call";
import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast";
import { getSenderName } from "./utils/event/getSenderName"; import { getSenderName } from "./utils/event/getSenderName";
import PosthogTrackers from "./PosthogTrackers.ts"; import PosthogTrackers from "./PosthogTrackers.ts";
@ -906,7 +905,6 @@ const stateHandlers: IHandlers = {
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": textForWidgetEvent, "im.vector.modular.widgets": textForWidgetEvent,
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent, [WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
[VoiceBroadcastInfoEventType]: textForVoiceBroadcastStoppedEvent,
}; };
// Add all the Mjolnir stuff to the renderer // Add all the Mjolnir stuff to the renderer

View file

@ -279,7 +279,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
if (!forceReset) { if (!forceReset) {
try { try {
this.setState({ phase: Phase.Loading }); this.setState({ phase: Phase.Loading });
backupInfo = await cli.getKeyBackupVersion(); backupInfo = await crypto.getKeyBackupInfo();
} catch (e) { } catch (e) {
logger.error("Error fetching backup data from server", e); logger.error("Error fetching backup data from server", e);
this.setState({ phase: Phase.LoadError }); this.setState({ phase: Phase.LoadError });

View file

@ -36,7 +36,7 @@ interface IState {
export default class EmbeddedPage extends React.PureComponent<IProps, IState> { export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private unmounted = false; private unmounted = false;
private dispatcherRef?: string; private dispatcherRef?: string;

View file

@ -34,6 +34,7 @@ import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -51,7 +52,7 @@ interface IState {
*/ */
class FilePanel extends React.Component<IProps, IState> { class FilePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
// This is used to track if a decrypted event was a live event and should be // This is used to track if a decrypted event was a live event and should be
// added to the timeline. // added to the timeline.
@ -104,7 +105,11 @@ class FilePanel extends React.Component<IProps, IState> {
} }
if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) { if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) {
this.state.timelineSet.addEventToTimeline(ev, timeline, false); this.state.timelineSet.addEventToTimeline(ev, timeline, {
fromCache: false,
addToState: false,
toStartOfTimeline: false,
});
} }
} }
@ -269,12 +274,10 @@ class FilePanel extends React.Component<IProps, IState> {
if (this.state.timelineSet) { if (this.state.timelineSet) {
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.File}
timelineRenderingType: TimelineRenderingType.File, narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
className="mx_FilePanel" className="mx_FilePanel"
@ -298,16 +301,11 @@ class FilePanel extends React.Component<IProps, IState> {
layout={Layout.Group} layout={Layout.Group}
/> />
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} else { } else {
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider {...this.context} timelineRenderingType={TimelineRenderingType.File}>
value={{
...this.context,
timelineRenderingType: TimelineRenderingType.File,
}}
>
<BaseCard <BaseCard
className="mx_FilePanel" className="mx_FilePanel"
onClose={this.props.onClose} onClose={this.props.onClose}
@ -315,7 +313,7 @@ class FilePanel extends React.Component<IProps, IState> {
> >
<Spinner /> <Spinner />
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -23,7 +23,6 @@ import classNames from "classnames";
import { isOnlyCtrlOrCmdKeyEvent, Key } from "../../Keyboard"; import { isOnlyCtrlOrCmdKeyEvent, Key } from "../../Keyboard";
import PageTypes from "../../PageTypes"; import PageTypes from "../../PageTypes";
import MediaDeviceHandler from "../../MediaDeviceHandler"; import MediaDeviceHandler from "../../MediaDeviceHandler";
import { fixupColorFonts } from "../../utils/FontManager";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { IMatrixClientCreds } from "../../MatrixClientPeg"; import { IMatrixClientCreds } from "../../MatrixClientPeg";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
@ -149,8 +148,6 @@ class LoggedInView extends React.Component<IProps, IState> {
MediaDeviceHandler.loadDevices(); MediaDeviceHandler.loadDevices();
fixupColorFonts();
this._roomView = React.createRef(); this._roomView = React.createRef();
this._resizeContainer = React.createRef(); this._resizeContainer = React.createRef();
this.resizeHandler = React.createRef(); this.resizeHandler = React.createRef();

View file

@ -119,7 +119,6 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig";
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
import GenericToast from "../views/toasts/GenericToast"; import GenericToast from "../views/toasts/GenericToast";
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
import { findDMForUser } from "../../utils/dm/findDMForUser"; import { findDMForUser } from "../../utils/dm/findDMForUser";
@ -227,7 +226,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private focusNext: FocusNextType; private focusNext: FocusNextType;
private subTitleStatus: string; private subTitleStatus: string;
private prevWindowWidth: number; private prevWindowWidth: number;
private voiceBroadcastResumer?: VoiceBroadcastResumer;
private readonly loggedInView = createRef<LoggedInViewType>(); private readonly loggedInView = createRef<LoggedInViewType>();
private dispatcherRef?: string; private dispatcherRef?: string;
@ -501,7 +499,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
window.removeEventListener("resize", this.onWindowResized); window.removeEventListener("resize", this.onWindowResized);
this.stores.accountPasswordStore.clearPassword(); this.stores.accountPasswordStore.clearPassword();
this.voiceBroadcastResumer?.destroy();
} }
private onWindowResized = (): void => { private onWindowResized = (): void => {
@ -651,10 +648,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break; break;
case "logout": case "logout":
LegacyCallHandler.instance.hangupAllCalls(); LegacyCallHandler.instance.hangupAllCalls();
Promise.all([ Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() =>
...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()), Lifecycle.logout(this.stores.oidcClientStore),
cleanUpBroadcasts(this.stores), );
]).finally(() => Lifecycle.logout(this.stores.oidcClientStore));
break; break;
case "require_registration": case "require_registration":
startAnyRegistrationFlow(payload as any); startAnyRegistrationFlow(payload as any);
@ -1638,7 +1634,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else { } else {
// otherwise check the server to see if there's a new one // otherwise check the server to see if there's a new one
try { try {
newVersionInfo = await cli.getKeyBackupVersion(); newVersionInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
if (newVersionInfo !== null) haveNewVersion = true; if (newVersionInfo !== null) haveNewVersion = true;
} catch (e) { } catch (e) {
logger.error("Saw key backup error but failed to check backup version!", e); logger.error("Saw key backup error but failed to check backup version!", e);
@ -1679,8 +1675,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
} }
}); });
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
} }
/** /**

View file

@ -196,7 +196,7 @@ interface IReadReceiptForUser {
*/ */
export default class MessagePanel extends React.Component<IProps, IState> { export default class MessagePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public static defaultProps = { public static defaultProps = {
disableGrouping: false, disableGrouping: false,

View file

@ -19,6 +19,7 @@ import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
onClose(): void; onClose(): void;
@ -33,7 +34,7 @@ interface IState {
*/ */
export default class NotificationPanel extends React.PureComponent<IProps, IState> { export default class NotificationPanel extends React.PureComponent<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private card = React.createRef<HTMLDivElement>(); private card = React.createRef<HTMLDivElement>();
@ -79,12 +80,10 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
} }
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.Notification}
timelineRenderingType: TimelineRenderingType.Notification, narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
header={_t("notifications|enable_prompt_toast_title")} header={_t("notifications|enable_prompt_toast_title")}
@ -99,7 +98,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />} {this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
{content} {content}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { MutableRefObject, ReactNode, useContext, useRef } from "react"; import React, { MutableRefObject, ReactNode, useRef } from "react";
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
@ -21,19 +21,7 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore"; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { SdkContextClass } from "../../contexts/SDKContext";
import {
useCurrentVoiceBroadcastPreRecording,
useCurrentVoiceBroadcastRecording,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackBody,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingPip,
VoiceBroadcastRecording,
VoiceBroadcastRecordingPip,
VoiceBroadcastSmallPlaybackBody,
} from "../../voice-broadcast";
import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
import { WidgetPip } from "../views/pips/WidgetPip"; import { WidgetPip } from "../views/pips/WidgetPip";
const SHOW_CALL_IN_STATES = [ const SHOW_CALL_IN_STATES = [
@ -46,9 +34,6 @@ const SHOW_CALL_IN_STATES = [
]; ];
interface IProps { interface IProps {
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
movePersistedElement: MutableRefObject<(() => void) | undefined>; movePersistedElement: MutableRefObject<(() => void) | undefined>;
} }
@ -245,52 +230,9 @@ class PipContainerInner extends React.Component<IProps, IState> {
this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId }); this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId });
} }
private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren {
const content =
this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? (
<VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} />
) : (
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} />
);
return ({ onStartMoving }) => (
<div key={`vb-playback-${voiceBroadcastPlayback.infoEvent.getId()}`} onMouseDown={onStartMoving}>
{content}
</div>
);
}
private createVoiceBroadcastPreRecordingPipContent(
voiceBroadcastPreRecording: VoiceBroadcastPreRecording,
): CreatePipChildren {
return ({ onStartMoving }) => (
<div key="vb-pre-recording" onMouseDown={onStartMoving}>
<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={voiceBroadcastPreRecording} />
</div>
);
}
private createVoiceBroadcastRecordingPipContent(
voiceBroadcastRecording: VoiceBroadcastRecording,
): CreatePipChildren {
return ({ onStartMoving }) => (
<div key={`vb-recording-${voiceBroadcastRecording.infoEvent.getId()}`} onMouseDown={onStartMoving}>
<VoiceBroadcastRecordingPip recording={voiceBroadcastRecording} />
</div>
);
}
public render(): ReactNode { public render(): ReactNode {
const pipMode = true; const pipMode = true;
let pipContent: Array<CreatePipChildren> = []; const pipContent: Array<CreatePipChildren> = [];
if (this.props.voiceBroadcastRecording) {
pipContent = [this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording)];
} else if (this.props.voiceBroadcastPreRecording) {
pipContent = [this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording)];
} else if (this.props.voiceBroadcastPlayback) {
pipContent = [this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback)];
}
if (this.state.primaryCall) { if (this.state.primaryCall) {
// get a ref to call inside the current scope // get a ref to call inside the current scope
@ -338,24 +280,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
} }
export const PipContainer: React.FC = () => { export const PipContainer: React.FC = () => {
const sdkContext = useContext(SDKContext);
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore);
const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore;
const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore);
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
const movePersistedElement = useRef<() => void>(); const movePersistedElement = useRef<() => void>();
return ( return <PipContainerInner movePersistedElement={movePersistedElement} />;
<PipContainerInner
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
voiceBroadcastRecording={currentVoiceBroadcastRecording}
movePersistedElement={movePersistedElement}
/>
);
}; };

View file

@ -63,7 +63,7 @@ interface IState {
export default class RightPanel extends React.Component<Props, IState> { export default class RightPanel extends React.Component<Props, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -109,10 +109,10 @@ export default class RightPanel extends React.Component<Props, IState> {
} }
// redraw the badge on the membership list // redraw the badge on the membership list
if (this.state.phase === RightPanelPhases.RoomMemberList) { if (this.state.phase === RightPanelPhases.MemberList) {
this.delayedUpdate(); this.delayedUpdate();
} else if ( } else if (
this.state.phase === RightPanelPhases.RoomMemberInfo && this.state.phase === RightPanelPhases.MemberInfo &&
member.userId === this.state.cardState?.member?.userId member.userId === this.state.cardState?.member?.userId
) { ) {
// refresh the member info (e.g. new power level) // refresh the member info (e.g. new power level)
@ -157,7 +157,7 @@ export default class RightPanel extends React.Component<Props, IState> {
const phase = this.props.overwriteCard?.phase ?? this.state.phase; const phase = this.props.overwriteCard?.phase ?? this.state.phase;
const cardState = this.props.overwriteCard?.state ?? this.state.cardState; const cardState = this.props.overwriteCard?.state ?? this.state.cardState;
switch (phase) { switch (phase) {
case RightPanelPhases.RoomMemberList: case RightPanelPhases.MemberList:
if (!!roomId) { if (!!roomId) {
card = ( card = (
<MemberList <MemberList
@ -170,22 +170,8 @@ export default class RightPanel extends React.Component<Props, IState> {
); );
} }
break; break;
case RightPanelPhases.SpaceMemberList:
if (!!cardState?.spaceId || !!roomId) {
card = (
<MemberList
roomId={cardState?.spaceId ?? roomId!}
key={cardState?.spaceId ?? roomId!}
onClose={this.onClose}
searchQuery={this.state.searchQuery}
onSearchQueryChanged={this.onSearchQueryChanged}
/>
);
}
break;
case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.MemberInfo:
case RightPanelPhases.SpaceMemberInfo:
case RightPanelPhases.EncryptionPanel: { case RightPanelPhases.EncryptionPanel: {
if (!!cardState?.member) { if (!!cardState?.member) {
const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined; const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined;
@ -203,8 +189,7 @@ export default class RightPanel extends React.Component<Props, IState> {
} }
break; break;
} }
case RightPanelPhases.Room3pidMemberInfo: case RightPanelPhases.ThreePidMemberInfo:
case RightPanelPhases.Space3pidMemberInfo:
if (!!cardState?.memberInfoEvent) { if (!!cardState?.memberInfoEvent) {
card = ( card = (
<ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} /> <ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} />

View file

@ -26,7 +26,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import RoomContext from "../../contexts/RoomContext"; import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
const DEBUG = false; const DEBUG = false;
let debuglog = function (msg: string): void {}; let debuglog = function (msg: string): void {};
@ -53,7 +53,7 @@ interface Props {
export const RoomSearchView = forwardRef<ScrollPanel, Props>( export const RoomSearchView = forwardRef<ScrollPanel, Props>(
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => {
const client = useContext(MatrixClientContext); const client = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext); const roomContext = useScopedRoomContext("showHiddenEvents");
const [highlights, setHighlights] = useState<string[] | null>(null); const [highlights, setHighlights] = useState<string[] | null>(null);
const [results, setResults] = useState<ISearchResults | null>(null); const [results, setResults] = useState<ISearchResults | null>(null);
const aborted = useRef(false); const aborted = useRef(false);

View file

@ -89,7 +89,7 @@ interface IState {
export default class RoomStatusBar extends React.PureComponent<IProps, IState> { export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
private unmounted = false; private unmounted = false;
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, useContext } from "react"; import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, JSX } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { import {
IRecommendedVersion, IRecommendedVersion,
@ -29,6 +29,7 @@ import {
MatrixError, MatrixError,
ISearchResults, ISearchResults,
THREAD_RELATION_TYPE, THREAD_RELATION_TYPE,
MatrixClient,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -54,7 +55,7 @@ import WidgetEchoStore from "../../stores/WidgetEchoStore";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import RoomContext, { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; import { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext";
import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils"; import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { IMatrixClientCreds } from "../../MatrixClientPeg"; import { IMatrixClientCreds } from "../../MatrixClientPeg";
@ -126,6 +127,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
const DEBUG = false; const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
@ -233,6 +235,11 @@ export interface IRoomState {
liveTimeline?: EventTimeline; liveTimeline?: EventTimeline;
narrow: boolean; narrow: boolean;
msc3946ProcessDynamicPredecessor: boolean; msc3946ProcessDynamicPredecessor: boolean;
/**
* Whether the room is encrypted or not.
* If null, we are still determining the encryption status.
*/
isRoomEncrypted: boolean | null;
canAskToJoin: boolean; canAskToJoin: boolean;
promptAskToJoin: boolean; promptAskToJoin: boolean;
@ -246,6 +253,7 @@ interface LocalRoomViewProps {
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
roomView: RefObject<HTMLElement>; roomView: RefObject<HTMLElement>;
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>; onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
mainSplitContentType: MainSplitContentType;
} }
/** /**
@ -255,7 +263,7 @@ interface LocalRoomViewProps {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
function LocalRoomView(props: LocalRoomViewProps): ReactElement { function LocalRoomView(props: LocalRoomViewProps): ReactElement {
const context = useContext(RoomContext); const context = useScopedRoomContext("room");
const room = context.room as LocalRoom; const room = context.room as LocalRoom;
const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0]; const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0];
let encryptionTile: ReactNode; let encryptionTile: ReactNode;
@ -323,6 +331,7 @@ interface ILocalRoomCreateLoaderProps {
localRoom: LocalRoom; localRoom: LocalRoom;
names: string; names: string;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
mainSplitContentType: MainSplitContentType;
} }
/** /**
@ -363,7 +372,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private roomViewBody = createRef<HTMLDivElement>(); private roomViewBody = createRef<HTMLDivElement>();
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) { public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
super(props, context); super(props, context);
@ -417,6 +426,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
canAskToJoin: this.askToJoinEnabled, canAskToJoin: this.askToJoinEnabled,
promptAskToJoin: false, promptAskToJoin: false,
viewRoomOpts: { buttons: [] }, viewRoomOpts: { buttons: [] },
isRoomEncrypted: null,
}; };
} }
@ -655,6 +665,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// the RoomView instance // the RoomView instance
if (initial) { if (initial) {
newState.room = this.context.client!.getRoom(newState.roomId) || undefined; newState.room = this.context.client!.getRoom(newState.roomId) || undefined;
newState.isRoomEncrypted = null;
if (newState.room) { if (newState.room) {
newState.showApps = this.shouldShowApps(newState.room); newState.showApps = this.shouldShowApps(newState.room);
this.onRoomLoaded(newState.room); this.onRoomLoaded(newState.room);
@ -697,6 +708,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (initial) { if (initial) {
this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek); this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek);
} }
// We don't block the initial setup but we want to make it early to not block the timeline rendering
const isRoomEncrypted = await this.getIsRoomEncrypted(newState.roomId);
this.setState({
isRoomEncrypted,
...(isRoomEncrypted &&
newState.roomId && { e2eStatus: RoomView.e2eStatusCache.get(newState.roomId) ?? E2EStatus.Warning }),
});
}; };
private onConnectedCalls = (): void => { private onConnectedCalls = (): void => {
@ -1214,18 +1233,18 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (payload.member) { if (payload.member) {
if (payload.push) { if (payload.push) {
RightPanelStore.instance.pushCard({ RightPanelStore.instance.pushCard({
phase: RightPanelPhases.RoomMemberInfo, phase: RightPanelPhases.MemberInfo,
state: { member: payload.member }, state: { member: payload.member },
}); });
} else { } else {
RightPanelStore.instance.setCards([ RightPanelStore.instance.setCards([
{ phase: RightPanelPhases.RoomSummary }, { phase: RightPanelPhases.RoomSummary },
{ phase: RightPanelPhases.RoomMemberList }, { phase: RightPanelPhases.MemberList },
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, { phase: RightPanelPhases.MemberInfo, state: { member: payload.member } },
]); ]);
} }
} else { } else {
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList);
} }
break; break;
case Action.View3pidInvite: case Action.View3pidInvite:
@ -1342,13 +1361,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange); this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.calculatePeekRules(room); this.calculatePeekRules(room);
this.updatePreviewUrlVisibility(room);
this.loadMembersIfJoined(room); this.loadMembersIfJoined(room);
this.calculateRecommendedVersion(room); this.calculateRecommendedVersion(room);
this.updateE2EStatus(room);
this.updatePermissions(room); this.updatePermissions(room);
this.checkWidgets(room); this.checkWidgets(room);
this.loadVirtualRoom(room); this.loadVirtualRoom(room);
this.updateRoomEncrypted(room);
if ( if (
this.getMainSplitContentType(room) !== MainSplitContentType.Timeline && this.getMainSplitContentType(room) !== MainSplitContentType.Timeline &&
@ -1377,6 +1395,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined; return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined;
} }
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
const crypto = this.context.client?.getCrypto();
if (!crypto || !roomId) return false;
return await crypto.isEncryptionEnabledInRoom(roomId);
}
private async calculateRecommendedVersion(room: Room): Promise<void> { private async calculateRecommendedVersion(room: Room): Promise<void> {
const upgradeRecommendation = await room.getRecommendedVersion(); const upgradeRecommendation = await room.getRecommendedVersion();
if (this.unmounted) return; if (this.unmounted) return;
@ -1409,12 +1434,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}); });
} }
private updatePreviewUrlVisibility({ roomId }: Room): void { private updatePreviewUrlVisibility(room: Room): void {
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit this.setState(({ isRoomEncrypted }) => ({
const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
this.setState({ }));
showUrlPreview: SettingsStore.getValue(key, roomId), }
});
private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean {
const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
return SettingsStore.getValue(key, roomId);
} }
private onRoom = (room: Room): void => { private onRoom = (room: Room): void => {
@ -1456,22 +1484,20 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}; };
private async updateE2EStatus(room: Room): Promise<void> { private async updateE2EStatus(room: Room): Promise<void> {
if (!this.context.client?.isRoomEncrypted(room.roomId)) return; if (!this.context.client || !this.state.isRoomEncrypted) return;
const e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client);
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
let e2eStatus = RoomView.e2eStatusCache.get(room.roomId) ?? E2EStatus.Warning;
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
this.setState({ e2eStatus });
if (this.context.client.getCrypto()) {
/* At this point, the user has encryption on and cross-signing on */
e2eStatus = await shieldStatusForRoom(this.context.client, room);
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
if (this.unmounted) return; if (this.unmounted) return;
this.setState({ e2eStatus }); this.setState({ e2eStatus });
} }
private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise<E2EStatus> {
let e2eStatus = RoomView.e2eStatusCache.get(room.roomId);
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
if (e2eStatus) this.setState({ e2eStatus });
e2eStatus = await shieldStatusForRoom(client, room);
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
return e2eStatus;
} }
private onUrlPreviewsEnabledChange = (): void => { private onUrlPreviewsEnabledChange = (): void => {
@ -1480,20 +1506,36 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
}; };
private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => { private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise<void> => {
// ignore if we don't have a room yet // ignore if we don't have a room yet
if (!this.state.room || this.state.room.roomId !== state.roomId) return; if (!this.state.room || this.state.room.roomId !== state.roomId || !this.context.client) return;
switch (ev.getType()) { switch (ev.getType()) {
case EventType.RoomTombstone: case EventType.RoomTombstone:
this.setState({ tombstone: this.getRoomTombstone() }); this.setState({ tombstone: this.getRoomTombstone() });
break; break;
case EventType.RoomEncryption: {
await this.updateRoomEncrypted();
break;
}
default: default:
this.updatePermissions(this.state.room); this.updatePermissions(this.state.room);
} }
}; };
private async updateRoomEncrypted(room = this.state.room): Promise<void> {
if (!room || !this.context.client) return;
const isRoomEncrypted = await this.getIsRoomEncrypted(room.roomId);
const newE2EStatus = isRoomEncrypted ? await this.cacheAndGetE2EStatus(room, this.context.client) : null;
this.setState({
isRoomEncrypted,
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
...(newE2EStatus && { e2eStatus: newE2EStatus }),
});
}
private onRoomStateUpdate = (state: RoomState): void => { private onRoomStateUpdate = (state: RoomState): void => {
// ignore members in other rooms // ignore members in other rooms
if (state.roomId !== this.state.room?.roomId) { if (state.roomId !== this.state.room?.roomId) {
@ -1959,35 +2001,41 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!this.state.room || !this.context?.client) return null; if (!this.state.room || !this.context?.client) return null;
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId()); const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<LocalRoomCreateLoader localRoom={localRoom} names={names} resizeNotifier={this.props.resizeNotifier} /> <LocalRoomCreateLoader
</RoomContext.Provider> localRoom={localRoom}
names={names}
resizeNotifier={this.props.resizeNotifier}
mainSplitContentType={this.state.mainSplitContentType}
/>
</ScopedRoomContextProvider>
); );
} }
private renderLocalRoomView(localRoom: LocalRoom): ReactNode { private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<LocalRoomView <LocalRoomView
localRoom={localRoom} localRoom={localRoom}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.permalinkCreator} permalinkCreator={this.permalinkCreator}
roomView={this.roomView} roomView={this.roomView}
onFileDrop={this.onFileDrop} onFileDrop={this.onFileDrop}
mainSplitContentType={this.state.mainSplitContentType}
/> />
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode { private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<WaitingForThirdPartyRoomView <WaitingForThirdPartyRoomView
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
roomView={this.roomView} roomView={this.roomView}
inviteEvent={inviteEvent} inviteEvent={inviteEvent}
/> />
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
@ -2027,6 +2075,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
public render(): ReactNode { public render(): ReactNode {
if (!this.context.client) return null; if (!this.context.client) return null;
const { isRoomEncrypted } = this.state;
const isRoomEncryptionLoading = isRoomEncrypted === null;
if (this.state.room instanceof LocalRoom) { if (this.state.room instanceof LocalRoom) {
if (this.state.room.state === LocalRoomState.CREATING) { if (this.state.room.state === LocalRoomState.CREATING) {
@ -2242,14 +2292,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let aux: JSX.Element | undefined; let aux: JSX.Element | undefined;
let previewBar; let previewBar;
if (this.state.timelineRenderingType === TimelineRenderingType.Search) { if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
if (!isRoomEncryptionLoading) {
aux = ( aux = (
<RoomSearchAuxPanel <RoomSearchAuxPanel
searchInfo={this.state.search} searchInfo={this.state.search}
onCancelClick={this.onCancelSearchClick} onCancelClick={this.onCancelSearchClick}
onSearchScopeChange={this.onSearchScopeChange} onSearchScopeChange={this.onSearchScopeChange}
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)} isRoomEncrypted={isRoomEncrypted}
/> />
); );
}
} else if (showRoomUpgradeBar) { } else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} />; aux = <RoomUpgradeWarningBar room={this.state.room} />;
} else if (myMembership !== KnownMembership.Join) { } else if (myMembership !== KnownMembership.Join) {
@ -2325,8 +2377,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let messageComposer; let messageComposer;
const showComposer = const showComposer =
!isRoomEncryptionLoading &&
// joined and not showing search results // joined and not showing search results
myMembership === KnownMembership.Join && !this.state.search; myMembership === KnownMembership.Join &&
!this.state.search;
if (showComposer) { if (showComposer) {
messageComposer = ( messageComposer = (
<MessageComposer <MessageComposer
@ -2367,7 +2421,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
highlightedEventId = this.state.initialEventId; highlightedEventId = this.state.initialEventId;
} }
const messagePanel = ( let messagePanel: JSX.Element | undefined;
if (!isRoomEncryptionLoading) {
messagePanel = (
<TimelinePanel <TimelinePanel
ref={this.gatherTimelinePanelRef} ref={this.gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
@ -2395,6 +2451,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
editState={this.state.editState} editState={this.state.editState}
/> />
); );
}
let topUnreadMessagesBar: JSX.Element | undefined; let topUnreadMessagesBar: JSX.Element | undefined;
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense // Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
@ -2415,7 +2472,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
); );
} }
const showRightPanel = this.state.room && this.state.showRightPanel; const showRightPanel = !isRoomEncryptionLoading && this.state.room && this.state.showRightPanel;
const rightPanel = showRightPanel ? ( const rightPanel = showRightPanel ? (
<RightPanel <RightPanel
@ -2516,7 +2573,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}> <div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
{showChatEffects && this.roomView.current && ( {showChatEffects && this.roomView.current && (
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} /> <EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
@ -2543,7 +2600,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</MainSplit> </MainSplit>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -208,7 +208,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
const storeIsShowingSpaceMembers = useCallback( const storeIsShowingSpaceMembers = useCallback(
() => () =>
RightPanelStore.instance.isOpenForRoom(space.roomId) && RightPanelStore.instance.isOpenForRoom(space.roomId) &&
RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.SpaceMemberList, RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.MemberList,
[space.roomId], [space.roomId],
); );
const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers); const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers);
@ -251,7 +251,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
} }
const onMembersClick = (): void => { const onMembersClick = (): void => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList }); RightPanelStore.instance.setCard({ phase: RightPanelPhases.MemberList });
}; };
return ( return (
@ -597,7 +597,7 @@ const SpaceSetupPrivateInvite: React.FC<{
export default class SpaceRoomView extends React.PureComponent<IProps, IState> { export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private dispatcherRef?: string; private dispatcherRef?: string;

View file

@ -20,7 +20,7 @@ import MatrixClientContext, { useMatrixClientContext } from "../../contexts/Matr
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton";
import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu"; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu";
import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
@ -30,6 +30,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import { clearRoomNotification } from "../../utils/notifications"; import { clearRoomNotification } from "../../utils/notifications";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -68,7 +69,7 @@ export const ThreadPanelHeader: React.FC<{
setFilterOption: (filterOption: ThreadFilterType) => void; setFilterOption: (filterOption: ThreadFilterType) => void;
}> = ({ filterOption, setFilterOption }) => { }> = ({ filterOption, setFilterOption }) => {
const mxClient = useMatrixClientContext(); const mxClient = useMatrixClientContext();
const roomContext = useRoomContext(); const roomContext = useScopedRoomContext("room");
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
const options: readonly ThreadPanelHeaderOption[] = [ const options: readonly ThreadPanelHeaderOption[] = [
{ {
@ -184,13 +185,11 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
}, [timelineSet, timelinePanel]); }, [timelineSet, timelinePanel]);
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...roomContext}
...roomContext, timelineRenderingType={TimelineRenderingType.ThreadsList}
timelineRenderingType: TimelineRenderingType.ThreadsList, showHiddenEvents={true}
showHiddenEvents: true, narrow={narrow}
narrow,
}}
> >
<BaseCard <BaseCard
header={ header={
@ -241,7 +240,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
</div> </div>
)} )}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
}; };
export default ThreadPanel; export default ThreadPanel;

View file

@ -51,6 +51,7 @@ import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/C
import Heading from "../views/typography/Heading"; import Heading from "../views/typography/Heading";
import { SdkContextClass } from "../../contexts/SDKContext"; import { SdkContextClass } from "../../contexts/SDKContext";
import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload"; import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
room: Room; room: Room;
@ -75,7 +76,7 @@ interface IState {
export default class ThreadView extends React.Component<IProps, IState> { export default class ThreadView extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private dispatcherRef?: string; private dispatcherRef?: string;
private layoutWatcherRef?: string; private layoutWatcherRef?: string;
@ -422,14 +423,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
} }
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.Thread}
timelineRenderingType: TimelineRenderingType.Thread, threadId={this.state.thread?.id}
threadId: this.state.thread?.id, liveTimeline={this.state?.thread?.timelineSet?.getLiveTimeline()}
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(), narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
className={classNames("mx_ThreadView mx_ThreadPanel", { className={classNames("mx_ThreadView mx_ThreadPanel", {
@ -463,7 +462,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
/> />
)} )}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -229,7 +229,7 @@ interface IEventIndexOpts {
*/ */
class TimelinePanel extends React.Component<IProps, IState> { class TimelinePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
// a map from room id to read marker event timestamp // a map from room id to read marker event timestamp
public static roomReadMarkerTsMap: Record<string, number> = {}; public static roomReadMarkerTsMap: Record<string, number> = {};

View file

@ -40,8 +40,6 @@ import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier"; import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
import PosthogTrackers from "../../PosthogTrackers"; import PosthogTrackers from "../../PosthogTrackers";
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
import { Icon as LiveIcon } from "../../../res/img/compound/live-8px.svg";
import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast";
import { SDKContext } from "../../contexts/SDKContext"; import { SDKContext } from "../../contexts/SDKContext";
import { shouldShowFeedback } from "../../utils/Feedback"; import { shouldShowFeedback } from "../../utils/Feedback";
import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg"; import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg";
@ -58,7 +56,6 @@ interface IState {
isDarkTheme: boolean; isDarkTheme: boolean;
isHighContrast: boolean; isHighContrast: boolean;
selectedSpace?: Room | null; selectedSpace?: Room | null;
showLiveAvatarAddon: boolean;
} }
const toRightOf = (rect: PartialDOMRect): MenuProps => { const toRightOf = (rect: PartialDOMRect): MenuProps => {
@ -79,7 +76,7 @@ const below = (rect: PartialDOMRect): MenuProps => {
export default class UserMenu extends React.Component<IProps, IState> { export default class UserMenu extends React.Component<IProps, IState> {
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
private dispatcherRef?: string; private dispatcherRef?: string;
private themeWatcherRef?: string; private themeWatcherRef?: string;
@ -94,7 +91,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
isDarkTheme: this.isUserOnDarkTheme(), isDarkTheme: this.isUserOnDarkTheme(),
isHighContrast: this.isUserOnHighContrastTheme(), isHighContrast: this.isUserOnHighContrastTheme(),
selectedSpace: SpaceStore.instance.activeSpaceRoom, selectedSpace: SpaceStore.instance.activeSpaceRoom,
showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(),
}; };
} }
@ -102,19 +98,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
return !!getHomePageUrl(SdkConfig.get(), this.context.client!); return !!getHomePageUrl(SdkConfig.get(), this.context.client!);
} }
private onCurrentVoiceBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => {
this.setState({
showLiveAvatarAddon: recording !== null,
});
};
public componentDidMount(): void { public componentDidMount(): void {
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
this.context.voiceBroadcastRecordingsStore.on(
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
this.onCurrentVoiceBroadcastRecordingChanged,
);
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
} }
@ -125,10 +111,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
this.context.voiceBroadcastRecordingsStore.off(
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
this.onCurrentVoiceBroadcastRecordingChanged,
);
} }
private isUserOnDarkTheme(): boolean { private isUserOnDarkTheme(): boolean {
@ -435,12 +417,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
name = <div className="mx_UserMenu_name">{displayName}</div>; name = <div className="mx_UserMenu_name">{displayName}</div>;
} }
const liveAvatarAddon = this.state.showLiveAvatarAddon ? (
<div className="mx_UserMenu_userAvatarLive" data-testid="user-menu-live-vb">
<LiveIcon className="mx_Icon_8" />
</div>
) : null;
return ( return (
<div className="mx_UserMenu"> <div className="mx_UserMenu">
<ContextMenuButton <ContextMenuButton
@ -459,7 +435,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
size={avatarSize + "px"} size={avatarSize + "px"}
className="mx_UserMenu_userAvatar_BaseAvatar" className="mx_UserMenu_userAvatar_BaseAvatar"
/> />
{liveAvatarAddon}
</div> </div>
{name} {name}
{this.renderContextMenu()} {this.renderContextMenu()}

View file

@ -32,7 +32,7 @@ interface IState {
export default class UserView extends React.Component<IProps, IState> { export default class UserView extends React.Component<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -82,7 +82,7 @@ export default class UserView extends React.Component<IProps, IState> {
} else if (this.state.member) { } else if (this.state.member) {
const panel = ( const panel = (
<RightPanel <RightPanel
overwriteCard={{ phase: RightPanelPhases.RoomMemberInfo, state: { member: this.state.member } }} overwriteCard={{ phase: RightPanelPhases.MemberInfo, state: { member: this.state.member } }}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
/> />
); );

View file

@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details.
import React, { RefObject } from "react"; import React, { RefObject } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { useRoomContext } from "../../contexts/RoomContext";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import ErrorBoundary from "../views/elements/ErrorBoundary"; import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomHeader from "../views/rooms/RoomHeader"; import RoomHeader from "../views/rooms/RoomHeader";
@ -19,6 +18,7 @@ import NewRoomIntro from "../views/rooms/NewRoomIntro";
import { UnwrappedEventTile } from "../views/rooms/EventTile"; import { UnwrappedEventTile } from "../views/rooms/EventTile";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
interface Props { interface Props {
roomView: RefObject<HTMLElement>; roomView: RefObject<HTMLElement>;
@ -32,7 +32,7 @@ interface Props {
* To avoid UTDs, users are shown a waiting room until the others have joined. * To avoid UTDs, users are shown a waiting room until the others have joined.
*/ */
export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resizeNotifier, inviteEvent }) => { export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resizeNotifier, inviteEvent }) => {
const context = useRoomContext(); const context = useScopedRoomContext("room");
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
return ( return (

View file

@ -75,6 +75,7 @@ interface State {
} }
export default class ForgotPassword extends React.Component<Props, State> { export default class ForgotPassword extends React.Component<Props, State> {
private unmounted = false;
private reset: PasswordReset; private reset: PasswordReset;
private fieldPassword: Field | null = null; private fieldPassword: Field | null = null;
private fieldPasswordConfirm: Field | null = null; private fieldPasswordConfirm: Field | null = null;
@ -108,14 +109,20 @@ export default class ForgotPassword extends React.Component<Props, State> {
} }
} }
private async checkServerLiveliness(serverConfig: ValidatedServerConfig): Promise<void> { public componentWillUnmount(): void {
this.unmounted = true;
}
private async checkServerLiveliness(serverConfig: ValidatedServerConfig): Promise<boolean> {
try { try {
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(serverConfig.hsUrl, serverConfig.isUrl); await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(serverConfig.hsUrl, serverConfig.isUrl);
if (this.unmounted) return false;
this.setState({ this.setState({
serverIsAlive: true, serverIsAlive: true,
}); });
} catch (e: any) { } catch (e: any) {
if (this.unmounted) return false;
const { serverIsAlive, serverDeadError } = AutoDiscoveryUtils.authComponentStateForError( const { serverIsAlive, serverDeadError } = AutoDiscoveryUtils.authComponentStateForError(
e, e,
"forgot_password", "forgot_password",
@ -124,7 +131,9 @@ export default class ForgotPassword extends React.Component<Props, State> {
serverIsAlive, serverIsAlive,
errorText: serverDeadError, errorText: serverDeadError,
}); });
return serverIsAlive;
} }
return true;
} }
private async onPhaseEmailInputSubmit(): Promise<void> { private async onPhaseEmailInputSubmit(): Promise<void> {
@ -292,10 +301,10 @@ export default class ForgotPassword extends React.Component<Props, State> {
}); });
// Refresh the server errors. Just in case the server came back online of went offline. // Refresh the server errors. Just in case the server came back online of went offline.
await this.checkServerLiveliness(this.props.serverConfig); const serverIsAlive = await this.checkServerLiveliness(this.props.serverConfig);
// Server error // Server error
if (!this.state.serverIsAlive) return; if (!serverIsAlive) return;
switch (this.state.phase) { switch (this.state.phase) {
case Phase.EnterEmail: case Phase.EnterEmail:

View file

@ -64,7 +64,7 @@ interface IState {
export default class SoftLogout extends React.Component<IProps, IState> { export default class SoftLogout extends React.Component<IProps, IState> {
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) { public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
super(props, context); super(props, context);

View file

@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { BaseGrouper } from "./BaseGrouper"; import { BaseGrouper } from "./BaseGrouper";
import MessagePanel, { WrappedEvent } from "../MessagePanel"; import MessagePanel, { WrappedEvent } from "../MessagePanel";
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import DateSeparator from "../../views/messages/DateSeparator"; import DateSeparator from "../../views/messages/DateSeparator";
@ -53,11 +52,6 @@ export class CreationGrouper extends BaseGrouper {
return false; return false;
} }
if (VoiceBroadcastInfoEventType === eventType) {
// always show voice broadcast info events in timeline
return false;
}
if (event.isState() && event.getSender() === createEvent.getSender()) { if (event.isState() && event.getSender() === createEvent.getSender()) {
return true; return true;
} }

View file

@ -1,45 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { MutableRefObject } from "react";
import { toLeftOrRightOf } from "../../structures/ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOptionList,
IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu";
interface Props {
containerRef: MutableRefObject<HTMLElement | null>;
currentDevice: MediaDeviceInfo | null;
devices: MediaDeviceInfo[];
onDeviceSelect: (device: MediaDeviceInfo) => void;
}
export const DevicesContextMenu: React.FC<Props> = ({ containerRef, currentDevice, devices, onDeviceSelect }) => {
const deviceOptions = devices.map((d: MediaDeviceInfo) => {
return (
<IconizedContextMenuRadio
key={d.deviceId}
active={d.deviceId === currentDevice?.deviceId}
onClick={() => onDeviceSelect(d)}
label={d.label}
/>
);
});
return (
<IconizedContextMenu
mountAsChild={false}
onFinished={() => {}}
{...(containerRef.current ? toLeftOrRightOf(containerRef.current.getBoundingClientRect(), 0) : {})}
>
<IconizedContextMenuOptionList>{deviceOptions}</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
};

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