mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-28 11:28:12 +08:00
Merge branch 'develop' into florianduros/rip-out-legacy-crypto/3-remove-isRoomEncrypted
This commit is contained in:
commit
cf4d2fd593
2
.github/workflows/backport.yml
vendored
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
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
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
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 }}
|
||||||
|
6
.github/workflows/dockerhub.yaml
vendored
6
.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:
|
||||||
|
8
.github/workflows/docs.yml
vendored
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
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
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
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
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
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
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
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: read
|
||||||
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
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
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
|
||||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -11,9 +11,13 @@ 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
|
||||||
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 +46,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
|
||||||
|
1
.github/workflows/release_prepare.yml
vendored
1
.github/workflows/release_prepare.yml
vendored
@ -17,6 +17,7 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
|
permissions: {} # Uses ELEMENT_BOT_TOKEN instead
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
5
.github/workflows/sonarqube.yml
vendored
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
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
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
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@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: success
|
state: success
|
||||||
|
2
.github/workflows/triage-assigned.yml
vendored
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
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
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
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
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
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
|
||||||
|
@ -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";
|
||||||
|
28
res/css/views/rooms/_UserIdentityWarning.pcss
Normal file
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));
|
||||||
|
}
|
7
src/@types/matrix-js-sdk.d.ts
vendored
7
src/@types/matrix-js-sdk.d.ts
vendored
@ -22,6 +22,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;
|
||||||
|
@ -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 ||
|
||||||
|
@ -275,7 +275,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = this.props.mxEvent.getContent<ImageContent>();
|
const content = this.props.mxEvent.getContent<ImageContent>();
|
||||||
let isAnimated = mayBeAnimated(content.info?.mimetype);
|
let isAnimated = content.info?.["org.matrix.msc4230.is_animated"] ?? mayBeAnimated(content.info?.mimetype);
|
||||||
|
|
||||||
// If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server
|
// If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server
|
||||||
// because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail.
|
// because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail.
|
||||||
@ -298,8 +298,15 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await this.props.mediaEventHelper!.sourceBlob.value;
|
// If we didn't receive the MSC4230 is_animated flag
|
||||||
if (!(await blobIsAnimated(content.info?.mimetype, blob))) {
|
// then we need to check if the image is animated by downloading it.
|
||||||
|
if (
|
||||||
|
content.info?.["org.matrix.msc4230.is_animated"] === false ||
|
||||||
|
!(await blobIsAnimated(
|
||||||
|
content.info?.mimetype,
|
||||||
|
await this.props.mediaEventHelper!.sourceBlob.value,
|
||||||
|
))
|
||||||
|
) {
|
||||||
isAnimated = false;
|
isAnimated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ import E2EIcon from "./E2EIcon";
|
|||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { aboveLeftOf, MenuProps } from "../../structures/ContextMenu";
|
import { aboveLeftOf, MenuProps } from "../../structures/ContextMenu";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
|
import { UserIdentityWarning } from "./UserIdentityWarning";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
||||||
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
|
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
|
||||||
@ -669,6 +670,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||||||
<Tooltip open={isTooltipOpen} description={formatTimeLeft(secondsLeft)} placement="bottom">
|
<Tooltip open={isTooltipOpen} description={formatTimeLeft(secondsLeft)} placement="bottom">
|
||||||
<div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
|
<div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
|
||||||
<div className="mx_MessageComposer_wrapper">
|
<div className="mx_MessageComposer_wrapper">
|
||||||
|
<UserIdentityWarning room={this.props.room} key={this.props.room.roomId} />
|
||||||
<ReplyPreview
|
<ReplyPreview
|
||||||
replyToEvent={this.props.replyToEvent}
|
replyToEvent={this.props.replyToEvent}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
|
328
src/components/views/rooms/UserIdentityWarning.tsx
Normal file
328
src/components/views/rooms/UserIdentityWarning.tsx
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
|
import { EventType, KnownMembership, MatrixEvent, Room, RoomStateEvent, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { CryptoApi, CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { Button, Separator } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
|
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||||
|
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
|
|
||||||
|
interface UserIdentityWarningProps {
|
||||||
|
/**
|
||||||
|
* The current room being viewed.
|
||||||
|
*/
|
||||||
|
room: Room;
|
||||||
|
/**
|
||||||
|
* The ID of the room being viewed. This is used to ensure that the
|
||||||
|
* component's state and references are cleared when the room changes.
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the given user's identity need to be approved?
|
||||||
|
*/
|
||||||
|
async function userNeedsApproval(crypto: CryptoApi, userId: string): Promise<boolean> {
|
||||||
|
const verificationStatus = await crypto.getUserVerificationStatus(userId);
|
||||||
|
return verificationStatus.needsUserApproval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the component is uninitialised, is in the process of initialising, or
|
||||||
|
* has completed initialising.
|
||||||
|
*/
|
||||||
|
enum InitialisationStatus {
|
||||||
|
Uninitialised,
|
||||||
|
Initialising,
|
||||||
|
Completed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a banner warning when there is an issue with a user's identity.
|
||||||
|
*
|
||||||
|
* Warns when an unverified user's identity has changed, and gives the user a
|
||||||
|
* button to acknowledge the change.
|
||||||
|
*/
|
||||||
|
export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }) => {
|
||||||
|
const cli = useMatrixClientContext();
|
||||||
|
const crypto = cli.getCrypto();
|
||||||
|
|
||||||
|
// The current room member that we are prompting the user to approve.
|
||||||
|
// `undefined` means we are not currently showing a prompt.
|
||||||
|
const [currentPrompt, setCurrentPrompt] = useState<RoomMember | undefined>(undefined);
|
||||||
|
|
||||||
|
// Whether or not we've already initialised the component by loading the
|
||||||
|
// room membership.
|
||||||
|
const initialisedRef = useRef<InitialisationStatus>(InitialisationStatus.Uninitialised);
|
||||||
|
// Which room members need their identity approved.
|
||||||
|
const membersNeedingApprovalRef = useRef<Map<string, RoomMember>>(new Map());
|
||||||
|
// For each user, we assign a sequence number to each verification status
|
||||||
|
// that we get, or fetch.
|
||||||
|
//
|
||||||
|
// Since fetching a verification status is asynchronous, we could get an
|
||||||
|
// update in the middle of fetching the verification status, which could
|
||||||
|
// mean that the status that we fetched is out of date. So if the current
|
||||||
|
// sequence number is not the same as the sequence number when we started
|
||||||
|
// the fetch, then we drop our fetched result, under the assumption that the
|
||||||
|
// update that we received is the most up-to-date version. If it is in fact
|
||||||
|
// not the most up-to-date version, then we should be receiving a new update
|
||||||
|
// soon with the newer value, so it will fix itself in the end.
|
||||||
|
//
|
||||||
|
// We also assign a sequence number when the user leaves the room, in order
|
||||||
|
// to prevent prompting about a user who leaves while we are fetching their
|
||||||
|
// verification status.
|
||||||
|
const verificationStatusSequencesRef = useRef<Map<string, number>>(new Map());
|
||||||
|
const incrementVerificationStatusSequence = (userId: string): number => {
|
||||||
|
const verificationStatusSequences = verificationStatusSequencesRef.current;
|
||||||
|
const value = verificationStatusSequences.get(userId);
|
||||||
|
const newValue = value === undefined ? 1 : value + 1;
|
||||||
|
verificationStatusSequences.set(userId, newValue);
|
||||||
|
return newValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the current prompt. Select a new user if needed, or hide the
|
||||||
|
// warning if we don't have anyone to warn about.
|
||||||
|
const updateCurrentPrompt = useCallback((): undefined => {
|
||||||
|
const membersNeedingApproval = membersNeedingApprovalRef.current;
|
||||||
|
// We have to do this in a callback to `setCurrentPrompt`
|
||||||
|
// because this function could have been called after an
|
||||||
|
// `await`, and the `currentPrompt` that this function would
|
||||||
|
// have may be outdated.
|
||||||
|
setCurrentPrompt((currentPrompt) => {
|
||||||
|
// If we're already displaying a warning, and that user still needs
|
||||||
|
// approval, continue showing that user.
|
||||||
|
if (currentPrompt && membersNeedingApproval.has(currentPrompt.userId)) return currentPrompt;
|
||||||
|
|
||||||
|
if (membersNeedingApproval.size === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We pick the user with the smallest user ID.
|
||||||
|
const keys = Array.from(membersNeedingApproval.keys()).sort((a, b) => a.localeCompare(b));
|
||||||
|
const selection = membersNeedingApproval.get(keys[0]!);
|
||||||
|
return selection;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add a user to the membersNeedingApproval map, and update the current
|
||||||
|
// prompt if necessary. The user will only be added if they are actually a
|
||||||
|
// member of the room. If they are not a member, this function will do
|
||||||
|
// nothing.
|
||||||
|
const addMemberNeedingApproval = useCallback(
|
||||||
|
(userId: string, member?: RoomMember): void => {
|
||||||
|
if (userId === cli.getUserId()) {
|
||||||
|
// We always skip our own user, because we can't pin our own identity.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
member = member ?? room.getMember(userId) ?? undefined;
|
||||||
|
if (!member) return;
|
||||||
|
|
||||||
|
membersNeedingApprovalRef.current.set(userId, member);
|
||||||
|
// We only select the prompt if we are done initialising,
|
||||||
|
// because we will select the prompt after we're done
|
||||||
|
// initialising, and we want to start by displaying a warning
|
||||||
|
// for the user with the smallest ID.
|
||||||
|
if (initialisedRef.current === InitialisationStatus.Completed) {
|
||||||
|
updateCurrentPrompt();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[cli, room, updateCurrentPrompt],
|
||||||
|
);
|
||||||
|
|
||||||
|
// For each user in the list check if their identity needs approval, and if
|
||||||
|
// so, add them to the membersNeedingApproval map and update the prompt if
|
||||||
|
// needed.
|
||||||
|
const addMembersWhoNeedApproval = useCallback(
|
||||||
|
async (members: RoomMember[]): Promise<void> => {
|
||||||
|
const verificationStatusSequences = verificationStatusSequencesRef.current;
|
||||||
|
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
for (const member of members) {
|
||||||
|
const userId = member.userId;
|
||||||
|
const sequenceNum = incrementVerificationStatusSequence(userId);
|
||||||
|
promises.push(
|
||||||
|
userNeedsApproval(crypto!, userId).then((needsApproval) => {
|
||||||
|
if (needsApproval) {
|
||||||
|
// Only actually update the list if we have the most
|
||||||
|
// recent value.
|
||||||
|
if (verificationStatusSequences.get(userId) === sequenceNum) {
|
||||||
|
addMemberNeedingApproval(userId, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
},
|
||||||
|
[crypto, addMemberNeedingApproval],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove a user from the membersNeedingApproval map, and update the current
|
||||||
|
// prompt if necessary.
|
||||||
|
const removeMemberNeedingApproval = useCallback(
|
||||||
|
(userId: string): void => {
|
||||||
|
membersNeedingApprovalRef.current.delete(userId);
|
||||||
|
updateCurrentPrompt();
|
||||||
|
},
|
||||||
|
[updateCurrentPrompt],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialise the component. Get the room members, check which ones need
|
||||||
|
// their identity approved, and pick one to display.
|
||||||
|
const loadMembers = useCallback(async (): Promise<void> => {
|
||||||
|
if (!crypto || initialisedRef.current !== InitialisationStatus.Uninitialised) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If encryption is not enabled in the room, we don't need to do
|
||||||
|
// anything. If encryption gets enabled later, we will retry, via
|
||||||
|
// onRoomStateEvent.
|
||||||
|
if (!(await crypto.isEncryptionEnabledInRoom(room.roomId))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initialisedRef.current = InitialisationStatus.Initialising;
|
||||||
|
|
||||||
|
const members = await room.getEncryptionTargetMembers();
|
||||||
|
await addMembersWhoNeedApproval(members);
|
||||||
|
|
||||||
|
updateCurrentPrompt();
|
||||||
|
initialisedRef.current = InitialisationStatus.Completed;
|
||||||
|
}, [crypto, room, addMembersWhoNeedApproval, updateCurrentPrompt]);
|
||||||
|
|
||||||
|
loadMembers().catch((e) => {
|
||||||
|
logger.error("Error initialising UserIdentityWarning:", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a user's verification status changes, we check if they need to be
|
||||||
|
// added/removed from the set of members needing approval.
|
||||||
|
const onUserVerificationStatusChanged = useCallback(
|
||||||
|
(userId: string, verificationStatus: UserVerificationStatus): void => {
|
||||||
|
// If we haven't started initialising, that means that we're in a
|
||||||
|
// room where we don't need to display any warnings.
|
||||||
|
if (initialisedRef.current === InitialisationStatus.Uninitialised) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementVerificationStatusSequence(userId);
|
||||||
|
|
||||||
|
if (verificationStatus.needsUserApproval) {
|
||||||
|
addMemberNeedingApproval(userId);
|
||||||
|
} else {
|
||||||
|
removeMemberNeedingApproval(userId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addMemberNeedingApproval, removeMemberNeedingApproval],
|
||||||
|
);
|
||||||
|
useTypedEventEmitter(cli, CryptoEvent.UserTrustStatusChanged, onUserVerificationStatusChanged);
|
||||||
|
|
||||||
|
// We watch for encryption events (since we only display warnings in
|
||||||
|
// encrypted rooms), and for membership changes (since we only display
|
||||||
|
// warnings for users in the room).
|
||||||
|
const onRoomStateEvent = useCallback(
|
||||||
|
async (event: MatrixEvent): Promise<void> => {
|
||||||
|
if (!crypto || event.getRoomId() !== room.roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventType = event.getType();
|
||||||
|
if (eventType === EventType.RoomEncryption && event.getStateKey() === "") {
|
||||||
|
// Room is now encrypted, so we can initialise the component.
|
||||||
|
return loadMembers().catch((e) => {
|
||||||
|
logger.error("Error initialising UserIdentityWarning:", e);
|
||||||
|
});
|
||||||
|
} else if (eventType !== EventType.RoomMember) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're processing an m.room.member event
|
||||||
|
|
||||||
|
if (initialisedRef.current === InitialisationStatus.Uninitialised) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = event.getStateKey();
|
||||||
|
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.getContent().membership === KnownMembership.Join ||
|
||||||
|
(event.getContent().membership === KnownMembership.Invite && room.shouldEncryptForInvitedMembers())
|
||||||
|
) {
|
||||||
|
// Someone's membership changed and we will now encrypt to them. If
|
||||||
|
// their identity needs approval, show a warning.
|
||||||
|
const member = room.getMember(userId);
|
||||||
|
if (member) {
|
||||||
|
await addMembersWhoNeedApproval([member]).catch((e) => {
|
||||||
|
logger.error("Error adding member in UserIdentityWarning:", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Someone's membership changed and we no longer encrypt to them.
|
||||||
|
// If we're showing a warning about them, we don't need to any more.
|
||||||
|
removeMemberNeedingApproval(userId);
|
||||||
|
incrementVerificationStatusSequence(userId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[crypto, room, addMembersWhoNeedApproval, removeMemberNeedingApproval, loadMembers],
|
||||||
|
);
|
||||||
|
useTypedEventEmitter(cli, RoomStateEvent.Events, onRoomStateEvent);
|
||||||
|
|
||||||
|
if (!crypto || !currentPrompt) return null;
|
||||||
|
|
||||||
|
const confirmIdentity = async (): Promise<void> => {
|
||||||
|
await crypto.pinCurrentUserIdentity(currentPrompt.userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_UserIdentityWarning">
|
||||||
|
<Separator />
|
||||||
|
<div className="mx_UserIdentityWarning_row">
|
||||||
|
<MemberAvatar member={currentPrompt} title={currentPrompt.userId} size="30px" />
|
||||||
|
<span className="mx_UserIdentityWarning_main">
|
||||||
|
{currentPrompt.rawDisplayName === currentPrompt.userId
|
||||||
|
? _t(
|
||||||
|
"encryption|pinned_identity_changed_no_displayname",
|
||||||
|
{ userId: currentPrompt.userId },
|
||||||
|
{
|
||||||
|
a: substituteATag,
|
||||||
|
b: substituteBTag,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: _t(
|
||||||
|
"encryption|pinned_identity_changed",
|
||||||
|
{ displayName: currentPrompt.rawDisplayName, userId: currentPrompt.userId },
|
||||||
|
{
|
||||||
|
a: substituteATag,
|
||||||
|
b: substituteBTag,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<Button kind="primary" size="sm" onClick={confirmIdentity}>
|
||||||
|
{_t("action|ok")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function substituteATag(sub: string): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<a href="https://element.io/help#encryption18" target="_blank" rel="noreferrer noopener">
|
||||||
|
{sub}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function substituteBTag(sub: string): React.ReactNode {
|
||||||
|
return <b>{sub}</b>;
|
||||||
|
}
|
@ -905,6 +905,8 @@
|
|||||||
"warning": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
|
"warning": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
|
||||||
},
|
},
|
||||||
"not_supported": "<not supported>",
|
"not_supported": "<not supported>",
|
||||||
|
"pinned_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) identity appears to have changed. <a>Learn more</a>",
|
||||||
|
"pinned_identity_changed_no_displayname": "<b>%(userId)s</b>'s identity appears to have changed. <a>Learn more</a>",
|
||||||
"recovery_method_removed": {
|
"recovery_method_removed": {
|
||||||
"description_1": "This session has detected that your Security Phrase and key for Secure Messages have been removed.",
|
"description_1": "This session has detected that your Security Phrase and key for Secure Messages have been removed.",
|
||||||
"description_2": "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.",
|
"description_2": "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.",
|
||||||
|
@ -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 { EncryptedFile } from "matrix-js-sdk/src/types";
|
import { ImageInfo } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
import { BlurhashEncoder } from "../BlurhashEncoder";
|
import { BlurhashEncoder } from "../BlurhashEncoder";
|
||||||
|
|
||||||
@ -15,19 +15,7 @@ type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
|||||||
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
|
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
|
||||||
|
|
||||||
interface IThumbnail {
|
interface IThumbnail {
|
||||||
info: {
|
info: ImageInfo;
|
||||||
thumbnail_info?: {
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
mimetype: string;
|
|
||||||
size: number;
|
|
||||||
};
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
[BLURHASH_FIELD]?: string;
|
|
||||||
thumbnail_url?: string;
|
|
||||||
thumbnail_file?: EncryptedFile;
|
|
||||||
};
|
|
||||||
thumbnail: Blob;
|
thumbnail: Blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,10 +474,8 @@ export default class ElectronPlatform extends BasePlatform {
|
|||||||
const url = super.getOidcCallbackUrl();
|
const url = super.getOidcCallbackUrl();
|
||||||
url.protocol = "io.element.desktop";
|
url.protocol = "io.element.desktop";
|
||||||
// Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
// Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
||||||
// Chrome seems to have a strange issue where non-standard protocols prevent URL object mutations on pathname
|
if (url.href.startsWith(`${url.protocol}://`)) {
|
||||||
// field, so we cannot mutate `pathname` reliably and instead have to rewrite the href manually.
|
url.href = url.href.replace("://", ":/");
|
||||||
if (url.pathname.startsWith("//")) {
|
|
||||||
url.href = url.href.replace(url.pathname, url.pathname.slice(1));
|
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,534 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
import {
|
||||||
|
EventType,
|
||||||
|
MatrixClient,
|
||||||
|
MatrixEvent,
|
||||||
|
Room,
|
||||||
|
RoomState,
|
||||||
|
RoomStateEvent,
|
||||||
|
RoomMember,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
import { act, render, screen, waitFor } from "jest-matrix-react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
|
import { stubClient } from "../../../../test-utils";
|
||||||
|
import { UserIdentityWarning } from "../../../../../src/components/views/rooms/UserIdentityWarning";
|
||||||
|
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||||
|
|
||||||
|
const ROOM_ID = "!room:id";
|
||||||
|
|
||||||
|
function mockRoom(): Room {
|
||||||
|
const room = {
|
||||||
|
getEncryptionTargetMembers: jest.fn(async () => []),
|
||||||
|
getMember: jest.fn((userId) => {}),
|
||||||
|
roomId: ROOM_ID,
|
||||||
|
shouldEncryptForInvitedMembers: jest.fn(() => true),
|
||||||
|
} as unknown as Room;
|
||||||
|
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockRoomMember(userId: string, name?: string): RoomMember {
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
name: name ?? userId,
|
||||||
|
rawDisplayName: name ?? userId,
|
||||||
|
roomId: ROOM_ID,
|
||||||
|
getMxcAvatarUrl: jest.fn(),
|
||||||
|
} as unknown as RoomMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dummyRoomState(): RoomState {
|
||||||
|
return new RoomState(ROOM_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the warning element, given the warning text (excluding the "Learn more"
|
||||||
|
* link). This is needed because the warning text contains a `<b>` tag, so the
|
||||||
|
* normal `getByText` doesn't work.
|
||||||
|
*/
|
||||||
|
function getWarningByText(text: string): Element {
|
||||||
|
return screen.getByText((content?: string, element?: Element | null): boolean => {
|
||||||
|
return (
|
||||||
|
!!element &&
|
||||||
|
element.classList.contains("mx_UserIdentityWarning_main") &&
|
||||||
|
element.textContent === text + " Learn more"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComponent(client: MatrixClient, room: Room) {
|
||||||
|
return render(<UserIdentityWarning room={room} key={ROOM_ID} />, {
|
||||||
|
wrapper: ({ ...rest }) => <MatrixClientContext.Provider value={client} {...rest} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("UserIdentityWarning", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
client = stubClient();
|
||||||
|
room = mockRoom();
|
||||||
|
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// This tests the basic functionality of the component. If we have a room
|
||||||
|
// member whose identity needs accepting, we should display a warning. When
|
||||||
|
// the "OK" button gets pressed, it should call `pinCurrentUserIdentity`.
|
||||||
|
it("displays a warning when a user's identity needs approval", async () => {
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||||
|
mockRoomMember("@alice:example.org", "Alice"),
|
||||||
|
]);
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||||
|
new UserVerificationStatus(false, false, false, true),
|
||||||
|
);
|
||||||
|
crypto.pinCurrentUserIdentity = jest.fn();
|
||||||
|
renderComponent(client, room);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByRole("button")!);
|
||||||
|
await waitFor(() => expect(crypto.pinCurrentUserIdentity).toHaveBeenCalledWith("@alice:example.org"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// We don't display warnings in non-encrypted rooms, but if encryption is
|
||||||
|
// enabled, then we should display a warning if there are any users whose
|
||||||
|
// identity need accepting.
|
||||||
|
it("displays pending warnings when encryption is enabled", async () => {
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||||
|
mockRoomMember("@alice:example.org", "Alice"),
|
||||||
|
]);
|
||||||
|
// Start the room off unencrypted. We shouldn't display anything.
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockResolvedValue(false);
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||||
|
new UserVerificationStatus(false, false, false, true),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderComponent(client, room);
|
||||||
|
await sleep(10); // give it some time to finish initialising
|
||||||
|
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow();
|
||||||
|
|
||||||
|
// Encryption gets enabled in the room. We should now warn that Alice's
|
||||||
|
// identity changed.
|
||||||
|
jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomEncryption,
|
||||||
|
state_key: "",
|
||||||
|
content: {
|
||||||
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@alice:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a user's identity needs approval, or has been approved, the display
|
||||||
|
// should update appropriately.
|
||||||
|
it("updates the display when identity changes", async () => {
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||||
|
mockRoomMember("@alice:example.org", "Alice"),
|
||||||
|
]);
|
||||||
|
jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice"));
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||||
|
new UserVerificationStatus(false, false, false, false),
|
||||||
|
);
|
||||||
|
renderComponent(client, room);
|
||||||
|
await sleep(10); // give it some time to finish initialising
|
||||||
|
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow();
|
||||||
|
|
||||||
|
// The user changes their identity, so we should show the warning.
|
||||||
|
act(() => {
|
||||||
|
client.emit(
|
||||||
|
CryptoEvent.UserTrustStatusChanged,
|
||||||
|
"@alice:example.org",
|
||||||
|
new UserVerificationStatus(false, false, false, true),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate the user's new identity having been approved, so we no
|
||||||
|
// longer show the warning.
|
||||||
|
act(() => {
|
||||||
|
client.emit(
|
||||||
|
CryptoEvent.UserTrustStatusChanged,
|
||||||
|
"@alice:example.org",
|
||||||
|
new UserVerificationStatus(false, false, false, false),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// We only display warnings about users in the room. When someone
|
||||||
|
// joins/leaves, we should update the warning appropriately.
|
||||||
|
describe("updates the display when a member joins/leaves", () => {
|
||||||
|
it("when invited users can see encrypted messages", async () => {
|
||||||
|
// Nobody in the room yet
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]);
|
||||||
|
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
|
||||||
|
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(true);
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||||
|
new UserVerificationStatus(false, false, false, true),
|
||||||
|
);
|
||||||
|
renderComponent(client, room);
|
||||||
|
await sleep(10); // give it some time to finish initialising
|
||||||
|
|
||||||
|
// Alice joins. Her identity needs approval, so we should show a warning.
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
state_key: "@alice:example.org",
|
||||||
|
content: {
|
||||||
|
membership: "join",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@alice:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob is invited. His identity needs approval, so we should show a
|
||||||
|
// warning for him after Alice's warning is resolved by her leaving.
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
state_key: "@bob:example.org",
|
||||||
|
content: {
|
||||||
|
membership: "invite",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@carol:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Alice leaves, so we no longer show her warning, but we will show
|
||||||
|
// a warning for Bob.
|
||||||
|
act(() => {
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
state_key: "@alice:example.org",
|
||||||
|
content: {
|
||||||
|
membership: "leave",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@alice:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(),
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when invited users cannot see encrypted messages", async () => {
|
||||||
|
// Nobody in the room yet
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]);
|
||||||
|
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
|
||||||
|
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false);
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||||
|
new UserVerificationStatus(false, false, false, true),
|
||||||
|
);
|
||||||
|
renderComponent(client, room);
|
||||||
|
await sleep(10); // give it some time to finish initialising
|
||||||
|
|
||||||
|
// Alice joins. Her identity needs approval, so we should show a warning.
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
state_key: "@alice:example.org",
|
||||||
|
content: {
|
||||||
|
membership: "join",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@alice:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob is invited. His identity needs approval, but we don't encrypt
|
||||||
|
// to him, so we won't show a warning. (When Alice leaves, the
|
||||||
|
// display won't be updated to show a warningfor Bob.)
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
state_key: "@bob:example.org",
|
||||||
|
content: {
|
||||||
|
membership: "invite",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@carol:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Alice leaves, so we no longer show her warning, and we don't show
|
||||||
|
// a warning for Bob.
|
||||||
|
act(() => {
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
state_key: "@alice:example.org",
|
||||||
|
content: {
|
||||||
|
membership: "leave",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@alice:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(),
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(() => getWarningByText("@bob:example.org's identity appears to have changed.")).toThrow(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when member leaves immediately after component is loaded", async () => {
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockImplementation(async () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Alice immediately leaves after we get the room
|
||||||
|
// membership, so we shouldn't show the warning any more
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
state_key: "@alice:example.org",
|
||||||
|
content: {
|
||||||
|
membership: "leave",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@alice:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return [mockRoomMember("@alice:example.org")];
|
||||||
|
});
|
||||||
|
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
|
||||||
|
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false);
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||||
|
new UserVerificationStatus(false, false, false, true),
|
||||||
|
);
|
||||||
|
renderComponent(client, room);
|
||||||
|
|
||||||
|
await sleep(10);
|
||||||
|
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when member leaves immediately after joining", async () => {
|
||||||
|
// Nobody in the room yet
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]);
|
||||||
|
jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId));
|
||||||
|
jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false);
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||||
|
new UserVerificationStatus(false, false, false, true),
|
||||||
|
);
|
||||||
|
renderComponent(client, room);
|
||||||
|
await sleep(10); // give it some time to finish initialising
|
||||||
|
|
||||||
|
// Alice joins. Her identity needs approval, so we should show a warning.
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
state_key: "@alice:example.org",
|
||||||
|
content: {
|
||||||
|
membership: "join",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@alice:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
// ... but she immediately leaves, so we shouldn't show the warning any more
|
||||||
|
client.emit(
|
||||||
|
RoomStateEvent.Events,
|
||||||
|
new MatrixEvent({
|
||||||
|
event_id: "$event_id",
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
state_key: "@alice:example.org",
|
||||||
|
content: {
|
||||||
|
membership: "leave",
|
||||||
|
},
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
sender: "@alice:example.org",
|
||||||
|
}),
|
||||||
|
dummyRoomState(),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
await sleep(10); // give it some time to finish
|
||||||
|
expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// When we have multiple users whose identity needs approval, one user's
|
||||||
|
// identity no longer needs approval (e.g. their identity was approved),
|
||||||
|
// then we show the next one.
|
||||||
|
it("displays the next user when the current user's identity is approved", async () => {
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||||
|
mockRoomMember("@alice:example.org", "Alice"),
|
||||||
|
mockRoomMember("@bob:example.org"),
|
||||||
|
]);
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue(
|
||||||
|
new UserVerificationStatus(false, false, false, true),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderComponent(client, room);
|
||||||
|
// We should warn about Alice's identity first.
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate Alice's new identity having been approved, so now we warn
|
||||||
|
// about Bob's identity.
|
||||||
|
act(() => {
|
||||||
|
client.emit(
|
||||||
|
CryptoEvent.UserTrustStatusChanged,
|
||||||
|
"@alice:example.org",
|
||||||
|
new UserVerificationStatus(false, false, false, false),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we get an update for a user's verification status while we're fetching
|
||||||
|
// that user's verification status, we should display based on the updated
|
||||||
|
// value.
|
||||||
|
describe("handles races between fetching verification status and receiving updates", () => {
|
||||||
|
// First case: check that if the update says that the user identity
|
||||||
|
// needs approval, but the fetch says it doesn't, we show the warning.
|
||||||
|
it("update says identity needs approval", async () => {
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||||
|
mockRoomMember("@alice:example.org", "Alice"),
|
||||||
|
]);
|
||||||
|
jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice"));
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockImplementation(async () => {
|
||||||
|
act(() => {
|
||||||
|
client.emit(
|
||||||
|
CryptoEvent.UserTrustStatusChanged,
|
||||||
|
"@alice:example.org",
|
||||||
|
new UserVerificationStatus(false, false, false, true),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Promise.resolve(new UserVerificationStatus(false, false, false, false));
|
||||||
|
});
|
||||||
|
renderComponent(client, room);
|
||||||
|
await sleep(10); // give it some time to finish initialising
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Second case: check that if the update says that the user identity
|
||||||
|
// doesn't needs approval, but the fetch says it does, we don't show the
|
||||||
|
// warning.
|
||||||
|
it("update says identity doesn't need approval", async () => {
|
||||||
|
jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([
|
||||||
|
mockRoomMember("@alice:example.org", "Alice"),
|
||||||
|
]);
|
||||||
|
jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice"));
|
||||||
|
const crypto = client.getCrypto()!;
|
||||||
|
jest.spyOn(crypto, "getUserVerificationStatus").mockImplementation(async () => {
|
||||||
|
act(() => {
|
||||||
|
client.emit(
|
||||||
|
CryptoEvent.UserTrustStatusChanged,
|
||||||
|
"@alice:example.org",
|
||||||
|
new UserVerificationStatus(false, false, false, false),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Promise.resolve(new UserVerificationStatus(false, false, false, true));
|
||||||
|
});
|
||||||
|
renderComponent(client, room);
|
||||||
|
await sleep(10); // give it some time to finish initialising
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(() =>
|
||||||
|
getWarningByText("Alice's (@alice:example.org) identity appears to have changed."),
|
||||||
|
).toThrow(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user