Merge branch 'develop' into dbkr/key_backup_by_default
33
.github/actions/download-verify-element-tarball/action.yml
vendored
Normal 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
|
@ -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"
|
||||||
|
|
2
.github/workflows/backport.yml
vendored
|
@ -7,6 +7,8 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
|
|
||||||
|
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backport:
|
backport:
|
||||||
name: Backport
|
name: Backport
|
||||||
|
|
1
.github/workflows/build.yml
vendored
|
@ -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 }}"
|
||||||
|
|
1
.github/workflows/build_debian.yaml
vendored
|
@ -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
|
||||||
|
|
5
.github/workflows/build_develop.yml
vendored
|
@ -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
|
@ -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 }}
|
10
.github/workflows/dockerhub.yaml
vendored
|
@ -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
|
||||||
|
|
8
.github/workflows/docs.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
.github/workflows/end-to-end-tests.yaml
vendored
|
@ -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"
|
||||||
|
|
1
.github/workflows/issue_closed.yml
vendored
|
@ -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
|
||||||
|
|
1
.github/workflows/localazy_download.yaml
vendored
|
@ -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
|
||||||
|
|
1
.github/workflows/localazy_upload.yaml
vendored
|
@ -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
|
||||||
|
|
5
.github/workflows/netlify.yaml
vendored
|
@ -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
|
||||||
|
|
1
.github/workflows/pending-reviews.yaml
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
3
.github/workflows/pull_request.yaml
vendored
|
@ -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 }}
|
||||||
|
|
|
@ -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
|
||||||
|
|
3
.github/workflows/release-drafter.yml
vendored
|
@ -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
|
||||||
|
|
1
.github/workflows/release-gitflow.yml
vendored
|
@ -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
|
||||||
|
|
7
.github/workflows/release.yml
vendored
|
@ -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
|
||||||
|
|
16
.github/workflows/release_prepare.yml
vendored
|
@ -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
|
||||||
|
|
5
.github/workflows/sonarqube.yml
vendored
|
@ -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 }}
|
||||||
|
|
4
.github/workflows/static_analysis.yaml
vendored
|
@ -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: |
|
||||||
|
|
3
.github/workflows/sync-labels.yml
vendored
|
@ -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
|
||||||
|
|
6
.github/workflows/tests.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/triage-assigned.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/triage-incoming.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/triage-labelled.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
4
.github/workflows/triage-unlabelled.yml
vendored
|
@ -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') }}
|
||||||
|
|
1
.github/workflows/update-jitsi.yml
vendored
|
@ -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
|
||||||
|
|
1
.github/workflows/update-topics.yaml
vendored
|
@ -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
|
||||||
|
|
20
CHANGELOG.md
|
@ -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
|
||||||
|
|
|
@ -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(),
|
|
||||||
};
|
|
|
@ -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.
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
17
package.json
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 975 KiB After Width: | Height: | Size: 975 KiB |
Before Width: | Height: | Size: 1 MiB After Width: | Height: | Size: 1 MiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
@ -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";
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
28
res/css/views/rooms/_UserIdentityWarning.pcss
Normal 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));
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
|
||||||
|
|
||||||
##############################
|
|
|
@ -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
|
|
13
src/@types/matrix-js-sdk.d.ts
vendored
|
@ -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 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ||
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> = {};
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|