diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index c36793bd2c..ae2cf6294d 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -18,9 +18,10 @@
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
-/src/models/Call.ts @element-hq/element-call-reviewers
-/src/call-types.ts @element-hq/element-call-reviewers
-/src/components/views/voip @element-hq/element-call-reviewers
+/src/models/Call.ts @element-hq/element-call-reviewers
+/src/call-types.ts @element-hq/element-call-reviewers
+/src/components/views/voip @element-hq/element-call-reviewers
+/playwright/e2e/voip/element-call.spec.ts @element-hq/element-call-reviewers
# Ignore translations as those will be updated by GHA for Localazy download
/src/i18n/strings
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7d3cfcf72f..ee6d9526ac 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,8 +10,7 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# develop pushes and repository_dispatch handled in build_develop.yaml
env:
- # These must be set for fetchdep.sh to get the right branch
- REPOSITORY: ${{ github.repository }}
+ # This must be set for fetchdep.sh to get the right branch
PR_NUMBER: ${{ github.event.pull_request.number }}
permissions: {} # No permissions required
jobs:
@@ -45,7 +44,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
# Disable cache on Windows as it is slower than not caching
# https://github.com/actions/setup-node/issues/975
@@ -56,15 +55,7 @@ jobs:
- run: yarn config set network-timeout 300000
- name: Fetch layered build
- id: layered_build
- env:
- # tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
- JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
- run: |
- scripts/layered.sh
- JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
- VECTOR_SHA=$(git rev-parse --short=12 HEAD)
- echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
+ run: ./scripts/layered.sh
- name: Copy config
run: cp element.io/develop/config.json config.json
@@ -72,9 +63,7 @@ jobs:
- name: Build
env:
CI_PACKAGE: true
- VERSION: "${{ steps.layered_build.outputs.VERSION }}"
- run: |
- yarn build
+ run: VERSION=$(scripts/get-version-from-git.sh) yarn build
- name: Upload Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml
index 5a7bf7630d..924d1bdd1d 100644
--- a/.github/workflows/build_develop.yml
+++ b/.github/workflows/build_develop.yml
@@ -28,7 +28,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 42fa0c0701..62a582249f 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -33,7 +33,7 @@ jobs:
repository: matrix-org/matrix-js-sdk
path: matrix-js-sdk
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
cache-dependency-path: element-web/yarn.lock
diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml
index 8e790d0fde..40f6d15e94 100644
--- a/.github/workflows/end-to-end-tests.yaml
+++ b/.github/workflows/end-to-end-tests.yaml
@@ -54,21 +54,16 @@ jobs:
with:
repository: element-hq/element-web
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
- name: Fetch layered build
- id: layered_build
env:
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
- run: |
- scripts/layered.sh
- JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
- VECTOR_SHA=$(git rev-parse --short=12 HEAD)
- echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
+ run: scripts/layered.sh
- name: Copy config
run: cp element.io/develop/config.json config.json
@@ -76,9 +71,7 @@ jobs:
- name: Build
env:
CI_PACKAGE: true
- VERSION: "${{ steps.layered_build.outputs.VERSION }}"
- run: |
- yarn build
+ run: VERSION=$(scripts/get-version-from-git.sh) yarn build
- name: Upload Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
@@ -89,7 +82,7 @@ jobs:
- name: Calculate runner variables
id: runner-vars
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
@@ -140,7 +133,7 @@ jobs:
name: webapp
path: webapp
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
cache-dependency-path: yarn.lock
@@ -207,7 +200,7 @@ jobs:
persist-credentials: false
repository: element-hq/element-web
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
if: inputs.skip != true
with:
cache: "yarn"
diff --git a/.github/workflows/issue_closed.yml b/.github/workflows/issue_closed.yml
index 249f1eb342..375c2e7184 100644
--- a/.github/workflows/issue_closed.yml
+++ b/.github/workflows/issue_closed.yml
@@ -10,7 +10,7 @@ jobs:
name: Tidy closed issues
runs-on: ubuntu-24.04
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: main
with:
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
@@ -142,7 +142,7 @@ jobs:
});
}
}
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
name: Close duplicate as Not Planned
if: steps.main.outputs.closeAsNotPlanned
with:
diff --git a/.github/workflows/pending-reviews.yaml b/.github/workflows/pending-reviews.yaml
index 199eb60daa..0474e60aa0 100644
--- a/.github/workflows/pending-reviews.yaml
+++ b/.github/workflows/pending-reviews.yaml
@@ -16,7 +16,7 @@ jobs:
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
ROOM_ID: ${{ secrets.ROOM_ID }}
diff --git a/.github/workflows/pull_request_base_branch.yaml b/.github/workflows/pull_request_base_branch.yaml
index fbdebfbed0..e79c37783b 100644
--- a/.github/workflows/pull_request_base_branch.yaml
+++ b/.github/workflows/pull_request_base_branch.yaml
@@ -8,7 +8,7 @@ jobs:
name: Check PR base branch
runs-on: ubuntu-24.04
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const baseBranch = context.payload.pull_request.base.ref;
diff --git a/.github/workflows/shared-component-visual-tests.yaml b/.github/workflows/shared-component-visual-tests.yaml
index 180d6bcbab..631529320f 100644
--- a/.github/workflows/shared-component-visual-tests.yaml
+++ b/.github/workflows/shared-component-visual-tests.yaml
@@ -26,7 +26,7 @@ jobs:
persist-credentials: false
repository: element-hq/element-web
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml
index c4aa773070..db75583821 100644
--- a/.github/workflows/static_analysis.yaml
+++ b/.github/workflows/static_analysis.yaml
@@ -12,8 +12,7 @@ concurrency:
cancel-in-progress: true
env:
- # These must be set for fetchdep.sh to get the right branch
- REPOSITORY: ${{ github.repository }}
+ # This must be set for fetchdep.sh to get the right branch
PR_NUMBER: ${{ github.event.pull_request.number }}
permissions: {} # No permissions required
@@ -25,7 +24,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -70,7 +69,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -88,7 +87,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -106,7 +105,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -124,7 +123,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a6f3b3ae70..98e5c736ef 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -44,7 +44,7 @@ jobs:
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
- name: Yarn cache
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: "lts/*"
cache: "yarn"
diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml
index 3ddc1b65ae..496cfd53df 100644
--- a/.github/workflows/triage-labelled.yml
+++ b/.github/workflows/triage-labelled.yml
@@ -27,7 +27,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
contains(github.event.issue.labels.*.name, 'A-Element-Call')
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
github.rest.issues.addLabels({
@@ -44,7 +44,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'good first issue') ||
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
github.rest.issues.addLabels({
diff --git a/.github/workflows/triage-stale.yml b/.github/workflows/triage-stale.yml
index 51120c336e..768dad22b1 100644
--- a/.github/workflows/triage-stale.yml
+++ b/.github/workflows/triage-stale.yml
@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
+ - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10
with:
operations-per-run: 100
diff --git a/.github/workflows/triage-unlabelled.yml b/.github/workflows/triage-unlabelled.yml
index d3bda6df1f..71396be804 100644
--- a/.github/workflows/triage-unlabelled.yml
+++ b/.github/workflows/triage-unlabelled.yml
@@ -5,44 +5,25 @@ on:
types: [unlabeled]
permissions: {}
jobs:
- Move_Unabeled_Issue_On_Project_Board:
+ move_no_longer_needs_info_issues:
name: Move no longer X-Needs-Info issues to Triaged
runs-on: ubuntu-24.04
- permissions:
- repository-projects: read
if: >
- ${{
- !contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}
- env:
- BOARD_NAME: "Issue triage"
- OWNER: ${{ github.repository_owner }}
- REPO: ${{ github.event.repository.name }}
- ISSUE: ${{ github.event.issue.number }}
+ !contains(github.event.issue.labels.*.name, 'X-Needs-Info')
steps:
- - name: Check if issue is already in "${{ env.BOARD_NAME }}"
- run: |
- json=$(curl -s -H 'Content-Type: application/json' -H "Authorization: bearer ${{ secrets.GITHUB_TOKEN }}" -X POST -d '{"query": "query($issue: Int!, $owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { projectCards { nodes { project { name } isArchived } } } } } ", "variables" : "{ \"issue\": '${ISSUE}', \"owner\": \"'${OWNER}'\", \"repo\": \"'${REPO}'\" }" }' https://api.github.com/graphql)
- if echo $json | jq '.data.repository.issue.projectCards.nodes | length'; then
- if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].project.name') =~ "${BOARD_NAME}" ]]; then
- if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].isArchived') == 'true' ]]; then
- echo "Issue is already in Project '$BOARD_NAME', but is archived - skipping workflow";
- echo "SKIP_ACTION=true" >> $GITHUB_ENV
- else
- echo "Issue is already in Project '$BOARD_NAME', proceeding";
- echo "ALREADY_IN_BOARD=true" >> $GITHUB_ENV
- fi
- else
- echo "Issue is not in project '$BOARD_NAME', cancelling this workflow"
- echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV
- fi
- fi
- - name: Move issue
- uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36
- if: ${{ env.ALREADY_IN_BOARD == 'true' && env.SKIP_ACTION != 'true' }}
+ - id: set_fields
+ uses: nipe0324/update-project-v2-item-field@c4af58452d1c5a788c1ea4f20e073fa722ec4a6b #v2.0.2
with:
- project: Issue triage
- column: Triaged
- repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+ project-url: ${{ env.PROJECT_URL }}
+ github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+ skip-update-script: |
+ const isIssue = item.type === 'ISSUE'
+ const status = item.fieldValues['Status']
+ return !isIssue || status !== 'Needs info'
+ field-name: Status
+ field-value: "Triaged"
+ env:
+ PROJECT_URL: https://github.com/orgs/element-hq/projects/120
remove_Z-Labs_label:
name: Remove Z-Labs label when features behind labs flags are removed
@@ -62,7 +43,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
contains(github.event.issue.labels.*.name, 'Z-Labs')
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
github.rest.issues.removeLabel({
diff --git a/.github/workflows/update-jitsi.yml b/.github/workflows/update-jitsi.yml
index 0a764699e7..eda2137cdd 100644
--- a/.github/workflows/update-jitsi.yml
+++ b/.github/workflows/update-jitsi.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
diff --git a/.github/workflows/update-topics.yaml b/.github/workflows/update-topics.yaml
index 7bf751384d..85b9632b45 100644
--- a/.github/workflows/update-topics.yaml
+++ b/.github/workflows/update-topics.yaml
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-24.04
environment: Matrix
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index aace77bf40..e83526aa03 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,12 +1,11 @@
-import type { ArgTypes, Preview, Decorator } from "@storybook/react-vite";
-import { addons } from "storybook/preview-api";
+import type { ArgTypes, Preview, Decorator, ReactRenderer, StrictArgs } from "@storybook/react-vite";
import "../res/css/shared.pcss";
import "./preview.css";
import React, { useLayoutEffect } from "react";
-import { FORCE_RE_RENDER } from "storybook/internal/core-events";
import { setLanguage } from "../src/shared-components/utils/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
+import { StoryContext } from "storybook/internal/csf";
export const globalTypes = {
theme: {
@@ -59,29 +58,9 @@ const withThemeProvider: Decorator = (Story, context) => {
);
};
-const LanguageSwitcher: React.FC<{
- language: string;
-}> = ({ language }) => {
- useLayoutEffect(() => {
- const changeLanguage = async (language: string) => {
- await setLanguage(language);
- // Force the component to re-render to apply the new language
- addons.getChannel().emit(FORCE_RE_RENDER);
- };
- changeLanguage(language);
- }, [language]);
-
- return null;
-};
-
-export const withLanguageProvider: Decorator = (Story, context) => {
- return (
- <>
-
-
- >
- );
-};
+async function languageLoader(context: StoryContext): Promise {
+ await setLanguage(context.globals.language);
+}
const withTooltipProvider: Decorator = (Story) => {
return (
@@ -93,7 +72,7 @@ const withTooltipProvider: Decorator = (Story) => {
const preview: Preview = {
tags: ["autodocs"],
- decorators: [withThemeProvider, withLanguageProvider, withTooltipProvider],
+ decorators: [withThemeProvider, withTooltipProvider],
parameters: {
options: {
storySort: {
@@ -108,6 +87,7 @@ const preview: Preview = {
test: "error",
},
},
+ loaders: [languageLoader],
};
export default preview;
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6aae07ecda..ac74c02a7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,41 @@
+Changes in [1.12.0](https://github.com/element-hq/element-web/releases/tag/v1.12.0) (2025-09-23)
+================================================================================================
+## 🦖 Deprecations
+
+* Remove remaining support for outdated .well-known settings ([#30702](https://github.com/element-hq/element-web/pull/30702)). Contributed by @richvdh.
+
+## ✨ Features
+
+* Add decline button to call notification toast (use new notification event) ([#30729](https://github.com/element-hq/element-web/pull/30729)). Contributed by @toger5.
+* Use the new room list by default ([#30640](https://github.com/element-hq/element-web/pull/30640)). Contributed by @langleyd.
+* "Verify this device" redesign ([#30596](https://github.com/element-hq/element-web/pull/30596)). Contributed by @uhoreg.
+* Set Element Call "intents" when starting and answering DM calls. ([#30730](https://github.com/element-hq/element-web/pull/30730)). Contributed by @Half-Shot.
+* Add axe compliance for new room list ([#30700](https://github.com/element-hq/element-web/pull/30700)). Contributed by @langleyd.
+* Stop ringing and remove toast if another device answers a RTC call. ([#30728](https://github.com/element-hq/element-web/pull/30728)). Contributed by @Half-Shot.
+* Automatically adjust history visibility when making a room private ([#30713](https://github.com/element-hq/element-web/pull/30713)). Contributed by @Half-Shot.
+* Release announcement for new room list ([#30675](https://github.com/element-hq/element-web/pull/30675)). Contributed by @dbkr.
+
+## 🐛 Bug Fixes
+
+* [Backport staging] Room list: make the filter resize correctly ([#30795](https://github.com/element-hq/element-web/pull/30795)). Contributed by @RiotRobot.
+* [Backport staging] Avoid flicker of the room list filter on resize ([#30794](https://github.com/element-hq/element-web/pull/30794)). Contributed by @RiotRobot.
+* Don't show release announcements while toasts are displayed ([#30770](https://github.com/element-hq/element-web/pull/30770)). Contributed by @dbkr.
+* Fix enabling key backup not working if there is an untrusted key backup ([#30707](https://github.com/element-hq/element-web/pull/30707)). Contributed by @Half-Shot.
+* Force `preload` to be false when setting an intent on an Element Call. ([#30759](https://github.com/element-hq/element-web/pull/30759)). Contributed by @Half-Shot.
+* Fix handling of 413 server response when uploading media ([#30737](https://github.com/element-hq/element-web/pull/30737)). Contributed by @hughns.
+* Make landmark navigation work with new room list ([#30747](https://github.com/element-hq/element-web/pull/30747)). Contributed by @dbkr.
+* Prevent voice message from displaying spurious errors ([#30736](https://github.com/element-hq/element-web/pull/30736)). Contributed by @florianduros.
+* Align default avatar and fix colors in composer pills ([#30739](https://github.com/element-hq/element-web/pull/30739)). Contributed by @florianduros.
+* Use configured URL for link to desktop app in message search settings ([#30742](https://github.com/element-hq/element-web/pull/30742)). Contributed by @t3chguy.
+* Fix history visibility when creating space rooms ([#30745](https://github.com/element-hq/element-web/pull/30745)). Contributed by @dbkr.
+* Check HTML-encoded quotes when handling translations for embedded pages (such as welcome.html) ([#30743](https://github.com/element-hq/element-web/pull/30743)). Contributed by @Half-Shot.
+* Fix local room encryption status always not enabled ([#30461](https://github.com/element-hq/element-web/pull/30461)). Contributed by @BillCarsonFr.
+* fix: make url in topic in room intro clickable ([#30686](https://github.com/element-hq/element-web/pull/30686)). Contributed by @florianduros.
+* Block change recovery key button while a change is ongoing. ([#30664](https://github.com/element-hq/element-web/pull/30664)). Contributed by @Half-Shot.
+* Hide advanced settings during room creation when `UIFeature.advancedSettings=false` ([#30684](https://github.com/element-hq/element-web/pull/30684)). Contributed by @florianduros.
+* A11y: improve accessibility of pinned messages ([#30558](https://github.com/element-hq/element-web/pull/30558)). Contributed by @florianduros.
+
+
Changes in [1.11.112](https://github.com/element-hq/element-web/releases/tag/v1.11.112) (2025-09-16)
====================================================================================================
Fix [CVE-2025-59161](https://www.cve.org/CVERecord?id=CVE-2025-59161) / [GHSA-m6c8-98f4-75rr](https://github.com/element-hq/element-web/security/advisories/GHSA-m6c8-98f4-75rr)
diff --git a/Dockerfile b/Dockerfile
index 7ad596d970..b12c3a2abf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
-# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
+# syntax=docker.io/docker/dockerfile:1.18-labs@sha256:79cdc14e1c220efb546ad14a8ebc816e3277cd72d27195ced5bebdd226dd1025
# Builder
-FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f7f28d1962d93cc096ea6327378d990284757fec281ce48e42436e7b4b167fa2 AS builder
+FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f8c398a3ad2612293e8827915c056ed0f5cc708b0f676274bb6c732e3c10f93d AS builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
RUN cp /src/config.sample.json /src/webapp/config.json
# App
-FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:0d019e980f83728002de7a6d8819d0d4af7179046d3946b8b37749953fbb28e6
+FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:14b127ed799301a21a1798516443c675237120c76b9a738d43c5e4747de4b1c9
# Need root user to install packages & manipulate the usr directory
USER root
diff --git a/docs/MVVM-v1.md b/docs/MVVM-v1.md
new file mode 100644
index 0000000000..bbd02ccb06
--- /dev/null
+++ b/docs/MVVM-v1.md
@@ -0,0 +1,69 @@
+# MVVM
+
+_Deprecated_, see [MVVM.md](./MVVM.md) for the current version.
+
+General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
+
+1. Model: This is where the business logic and data resides.
+2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
+3. View: This is the UI code itself and depends on the view model.
+
+If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
+
+### Practical guidelines for MVVM in element-web
+
+#### Model
+
+This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
+
+#### View Model
+
+1. View model is always a custom react hook named like `useFooViewModel()`.
+2. The return type of your view model (known as view state) must be defined as a typescript interface:
+ ```ts
+ inteface FooViewState {
+ somethingUseful: string;
+ somethingElse: BarType;
+ update: () => Promise
+ ...
+ }
+ ```
+3. Any react state that your UI needs must be in the view model.
+
+#### View
+
+1. Views are simple react components (eg: `FooView`).
+2. Views usually start by calling the view model hook, eg:
+ ```tsx
+ const FooView: React.FC = (props: IProps) => {
+ const vm = useFooViewModel();
+ ....
+ return(
+
+ {vm.somethingUseful}
+
+ );
+ }
+ ```
+3. Views are also allowed to accept the view model as a prop, eg:
+ ```tsx
+ const FooView: React.FC = ({ vm }: IProps) => {
+ ....
+ return(
+
+ {vm.somethingUseful}
+
+ );
+ }
+ ```
+4. Multiple views can share the same view model if necessary.
+
+### Benefits
+
+1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
+2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
+3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
+
+### Example
+
+We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
diff --git a/docs/MVVM.md b/docs/MVVM.md
index 9dfb8e4776..065b647c24 100644
--- a/docs/MVVM.md
+++ b/docs/MVVM.md
@@ -10,58 +10,80 @@ If you do MVVM right, your view should be dumb i.e it gets data from the view mo
### Practical guidelines for MVVM in element-web
+A first documentation and implementation of MVVM was done in [MVVM-v1.md](MVVM-v1.md). This v1 version is now deprecated and this document describes the current implementation.
+
#### Model
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
-#### View Model
-
-1. View model is always a custom react hook named like `useFooViewModel()`.
-2. The return type of your view model (known as view state) must be defined as a typescript interface:
- ```ts
- inteface FooViewState {
- somethingUseful: string;
- somethingElse: BarType;
- update: () => Promise
- ...
- }
- ```
-3. Any react state that your UI needs must be in the view model.
-
#### View
-1. Views are simple react components (eg: `FooView`).
-2. Views usually start by calling the view model hook, eg:
+1. Located in [`shared-components`](https://github.com/element-hq/element-web/tree/develop/src/shared-components). Develop it in storybook!
+2. Views are simple react components (eg: `FooView`).
+3. Views use [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) internally where the view model is the external store.
+4. Views should define the interface of the view model they expect:
+
```tsx
- const FooView: React.FC = (props: IProps) => {
- const vm = useFooViewModel();
- ....
- return(
-
- {vm.somethingUseful}
-
- );
+ // Snapshot is the return type of your view model
+ interface FooViewSnapshot {
+ value: string;
+ }
+
+ // To call function on the view model
+ interface FooViewActions {
+ doSomething: () => void;
+ }
+
+ // ViewModel is a type defining the methods needed for `useSyncExternalStore`
+ // https://github.com/element-hq/element-web/blob/develop/src/shared-components/ViewModel.ts
+ type FooViewModel = ViewModel & FooViewActions;
+
+ interface FooViewProps {
+ vm: FooViewModel;
+ }
+
+ function FooView({ vm }: FooViewProps) {
+ // useViewModel is a helper function that uses useSyncExternalStore under the hood
+ const { value } = useViewModel(vm);
+ return (
+
+ );
}
```
-3. Views are also allowed to accept the view model as a prop, eg:
- ```tsx
- const FooView: React.FC = ({ vm }: IProps) => {
- ....
- return(
-
- {vm.somethingUseful}
-
- );
+
+5. Multiple views can share the same view model if necessary.
+6. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/shared-components/audio/AudioPlayerView/AudioPlayerView.tsx)
+
+#### View Model
+
+1. A View model is a class extending [`BaseViewModel`](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/base/BaseViewModel.ts).
+2. Implements the interface defined in the view (e.g `FooViewModel` in the example above).
+3. View models define a snapshot type that defines the data the view will consume. The snapshot is immutable and can only be changed by calling `this.snapshot.set(...)` in the view model. This will trigger a re-render in the view.
+
+ ```ts
+ interface Props {
+ propsValue: string;
+ }
+
+ class FooViewModel extends BaseViewModel implements FooViewModel {
+ constructor(props: Props) {
+ // Call super with initial snapshot
+ super(props, { value: "initial" });
+ }
+
+ public doSomething() {
+ // Call this.snapshot.set to update the snapshot
+ this.snapshot.set({ value: "changed" });
+ }
}
```
-4. Multiple views can share the same view model if necessary.
+
+4. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/audio/AudioPlayerViewModel.ts)
### Benefits
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
-
-### Example
-
-We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
diff --git a/jest.config.ts b/jest.config.ts
index 3403ad6a0c..7054afa00e 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -41,7 +41,7 @@ const config: Config = {
"recorderWorkletFactory": "/__mocks__/empty.js",
"^fetch-mock$": "/node_modules/fetch-mock",
},
- transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
+ transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error)).+$"],
collectCoverageFrom: [
"/src/**/*.{js,ts,tsx}",
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is
diff --git a/package.json b/package.json
index f5b6464b60..c36ae5d4eb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "element-web",
- "version": "1.11.112",
+ "version": "1.12.0",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -75,11 +75,11 @@
"resolutions": {
"**/pretty-format/react-is": "19.1.1",
"@playwright/test": "1.54.2",
- "@types/react": "19.1.12",
+ "@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"oidc-client-ts": "3.3.0",
"jwt-decode": "4.0.0",
- "caniuse-lite": "1.0.30001724",
+ "caniuse-lite": "1.0.30001741",
"testcontainers": "^11.0.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
@@ -98,7 +98,7 @@
"@types/png-chunks-extract": "^1.0.2",
"@vector-im/compound-design-tokens": "^6.0.0",
"@vector-im/compound-web": "^8.1.2",
- "@vector-im/matrix-wysiwyg": "2.39.0",
+ "@vector-im/matrix-wysiwyg": "2.40.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
@@ -142,7 +142,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
- "posthog-js": "1.261.0",
+ "posthog-js": "1.265.1",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -159,7 +159,7 @@
"tar-js": "^0.3.0",
"temporal-polyfill": "^0.3.0",
"ua-parser-js": "1.0.40",
- "uuid": "^11.0.0",
+ "uuid": "^13.0.0",
"what-input": "^5.2.10"
},
"devDependencies": {
@@ -223,7 +223,7 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
- "@types/react": "19.1.12",
+ "@types/react": "19.1.13",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "19.1.9",
"@types/react-transition-group": "^4.4.0",
@@ -232,7 +232,6 @@
"@types/semver": "^7.5.8",
"@types/tar-js": "^0.3.5",
"@types/ua-parser-js": "^0.7.36",
- "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"babel-jest": "^29.0.0",
diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts
index eff4e0110d..998a5e49a4 100644
--- a/playwright/e2e/crypto/crypto.spec.ts
+++ b/playwright/e2e/crypto/crypto.spec.ts
@@ -24,7 +24,7 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "Start chat" }).click();
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
- await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
+ await page.getByRole("option", { name: bob.credentials.displayName }).click();
await expect(
page.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText("Bob"),
).toBeVisible();
diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts
index bd1c0ca691..2382a0c80e 100644
--- a/playwright/e2e/crypto/device-verification.spec.ts
+++ b/playwright/e2e/crypto/device-verification.spec.ts
@@ -201,6 +201,30 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
});
+ test("After cancelling verify with another device, I can try again #29882", async ({ page, app, credentials }) => {
+ // Regression test for https://github.com/element-hq/element-web/issues/29882
+
+ // Log in without verifying
+ await logIntoElement(page, credentials);
+ const authPage = page.locator(".mx_AuthPage");
+ await authPage.getByRole("button", { name: "Skip verification for now" }).click();
+ await authPage.getByRole("button", { name: "I'll verify later" }).click();
+ await page.waitForSelector(".mx_MatrixChat");
+
+ // Start to verify with "Use another device" but cancel
+ const settings = await app.settings.openUserSettings("Encryption");
+ await settings.getByRole("button", { name: "Verify this device" }).click();
+ await page.getByRole("button", { name: "Use another device" }).click();
+ await page.locator("#mx_Dialog_Container").getByRole("button", { name: "Close dialog" }).click();
+
+ // Start again
+ await settings.getByRole("button", { name: "Verify this device" }).click();
+
+ // We should be offered to use another device again.
+ // (In the bug, we were immediately told that verification has been cancelled.)
+ await expect(page.getByRole("button", { name: "Use another device" })).toBeVisible();
+ });
+
/** Helper for the three tests above which verify by recovery key */
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
await page.getByRole("button", { name: "Use recovery key" }).click();
diff --git a/playwright/e2e/invite/invite-dialog.spec.ts b/playwright/e2e/invite/invite-dialog.spec.ts
index 5c249f8d66..1c0cb3fa81 100644
--- a/playwright/e2e/invite/invite-dialog.spec.ts
+++ b/playwright/e2e/invite/invite-dialog.spec.ts
@@ -50,11 +50,9 @@ test.describe("Invite dialog", function () {
await expect(other.locator(".mx_InviteDialog_identityServer")).toBeVisible();
// Assert that the bot id is rendered properly
- await expect(
- other.locator(".mx_InviteDialog_tile_nameStack_userId").getByText(bot.credentials.userId),
- ).toBeVisible();
+ await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
- await other.locator(".mx_InviteDialog_tile_nameStack_name").getByText(botName).click();
+ await other.getByRole("option", { name: botName }).click();
await expect(
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
@@ -94,10 +92,8 @@ test.describe("Invite dialog", function () {
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
- await expect(
- other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId),
- ).toBeVisible();
- await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click();
+ await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
+ await other.getByRole("option", { name: botName }).click();
await expect(
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
diff --git a/playwright/e2e/voip/element-call.spec.ts b/playwright/e2e/voip/element-call.spec.ts
new file mode 100644
index 0000000000..1911843046
--- /dev/null
+++ b/playwright/e2e/voip/element-call.spec.ts
@@ -0,0 +1,339 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import type { EventType, Preset } from "matrix-js-sdk/src/matrix";
+import { SettingLevel } from "../../../src/settings/SettingLevel";
+import { test, expect } from "../../element-web-test";
+import type { Credentials } from "../../plugins/homeserver";
+import type { Bot } from "../../pages/bot";
+
+function assertCommonCallParameters(
+ url: URLSearchParams,
+ hash: URLSearchParams,
+ user: Credentials,
+ room: { roomId: string },
+): void {
+ expect(url.has("widgetId")).toEqual(true);
+ expect(url.has("parentUrl")).toEqual(true);
+
+ expect(hash.get("perParticipantE2EE")).toEqual("false");
+ expect(hash.get("userId")).toEqual(user.userId);
+ expect(hash.get("deviceId")).toEqual(user.deviceId);
+ expect(hash.get("roomId")).toEqual(room.roomId);
+ expect(hash.get("preload")).toEqual("false");
+}
+
+async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification") {
+ const resp = await bot.sendStateEvent(
+ roomId,
+ "org.matrix.msc3401.call.member",
+ {
+ application: "m.call",
+ call_id: "",
+ device_id: "OiDFxsZrjz",
+ expires: 180000000,
+ foci_preferred: [
+ {
+ livekit_alias: roomId,
+ livekit_service_url: "https://example.org",
+ type: "livekit",
+ },
+ ],
+ focus_active: {
+ focus_selection: "oldest_membership",
+ type: "livekit",
+ },
+ scope: "m.room",
+ },
+ `_@${bot.credentials.userId}_OiDFxsZrjz_m.call`,
+ );
+ if (!notification) {
+ return;
+ }
+ await bot.sendEvent(roomId, null, "org.matrix.msc4075.rtc.notification", {
+ "lifetime": 30000,
+ "m.mentions": {
+ room: true,
+ user_ids: [],
+ },
+ "m.relates_to": {
+ event_id: resp.event_id,
+ rel_type: "org.matrix.msc4075.rtc.notification.parent",
+ },
+ "notification_type": notification,
+ "sender_ts": 1758611895996,
+ });
+}
+
+test.describe("Element Call", () => {
+ test.use({
+ config: {
+ element_call: {
+ use_exclusively: false,
+ },
+ features: {
+ feature_group_calls: true,
+ },
+ },
+ displayName: "Alice",
+ botCreateOpts: {
+ autoAcceptInvites: true,
+ displayName: "Bob",
+ },
+ });
+
+ test.beforeEach(async ({ page, user, app }) => {
+ // Mock a widget page. It doesn't need to actually be Element Call.
+ await page.route("/widget.html", async (route) => {
+ await route.fulfill({
+ status: 200,
+ body: "
Hello world
",
+ });
+ });
+ await app.settings.setValue(
+ "Developer.elementCallUrl",
+ null,
+ SettingLevel.DEVICE,
+ new URL("/widget.html#", page.url()).toString(),
+ );
+ });
+
+ test.describe("Group Chat", () => {
+ test.use({
+ room: async ({ page, app, user, bot }, use) => {
+ const roomId = await app.client.createRoom({ name: "TestRoom", invite: [bot.credentials.userId] });
+ await use({ roomId });
+ },
+ });
+ test("should be able to start a video call", async ({ page, user, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+
+ await page.getByRole("button", { name: "Video call" }).click();
+ await page.getByRole("menuitem", { name: "Element Call" }).click();
+
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ await expect(frameUrlStr).toBeDefined();
+ // Ensure we set the correct parameters for ECall.
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+ expect(hash.get("intent")).toEqual("start_call");
+ expect(hash.get("skipLobby")).toEqual(null);
+ });
+
+ test("should be able to skip lobby by holding down shift", async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+
+ await page.getByRole("button", { name: "Video call" }).click();
+ await page.keyboard.down("Shift");
+ await page.getByRole("menuitem", { name: "Element Call" }).click();
+ await page.keyboard.up("Shift");
+
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+ expect(hash.get("intent")).toEqual("start_call");
+ expect(hash.get("skipLobby")).toEqual("true");
+ });
+
+ test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ // Allow bob to create a call
+ await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+ // Fake a start of a call
+ await sendRTCState(bot, room.roomId);
+ const button = page.getByTestId("join-call-button");
+ await expect(button).toBeInViewport({ timeout: 5000 });
+ // And test joining
+ await button.click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ console.log(frameUrlStr);
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+
+ expect(hash.get("intent")).toEqual("join_existing");
+ expect(hash.get("skipLobby")).toEqual(null);
+ });
+
+ [true, false].forEach((skipLobbyToggle) => {
+ test(
+ `should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
+ { tag: ["@screenshot"] },
+ async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ // Allow bob to create a call
+ await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+ // Fake a start of a call
+ await sendRTCState(bot, room.roomId, "notification");
+ const toast = page.locator(".mx_Toast_toast");
+ const button = toast.getByRole("button", { name: "Join" });
+ if (skipLobbyToggle) {
+ await toast.getByRole("switch").check();
+ await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-checked.png");
+ } else {
+ await toast.getByRole("switch").uncheck();
+ await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-unchecked.png");
+ }
+
+ // And test joining
+ await button.click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ console.log(frameUrlStr);
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+
+ expect(hash.get("intent")).toEqual("join_existing");
+ expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
+ },
+ );
+ });
+ });
+
+ test.describe("DMs", () => {
+ test.use({
+ room: async ({ page, app, user, bot }, use) => {
+ const roomId = await app.client.createRoom({
+ preset: "trusted_private_chat" as Preset.TrustedPrivateChat,
+ invite: [bot.credentials.userId],
+ });
+ await app.client.setAccountData("m.direct" as EventType.Direct, {
+ [bot.credentials.userId]: [roomId],
+ });
+ await use({ roomId });
+ },
+ });
+
+ test("should be able to start a video call", async ({ page, user, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+
+ await page.getByRole("button", { name: "Video call" }).click();
+ await page.getByRole("menuitem", { name: "Element Call" }).click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+ expect(hash.get("intent")).toEqual("start_call_dm");
+ expect(hash.get("skipLobby")).toEqual(null);
+ });
+
+ test("should be able to skip lobby by holding down shift", async ({ page, user, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+
+ await page.getByRole("button", { name: "Video call" }).click();
+ await page.keyboard.down("Shift");
+ await page.getByRole("menuitem", { name: "Element Call" }).click();
+ await page.keyboard.up("Shift");
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+ expect(hash.get("intent")).toEqual("start_call_dm");
+ expect(hash.get("skipLobby")).toEqual("true");
+ });
+
+ test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ // Allow bob to create a call
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+ // Fake a start of a call
+ await sendRTCState(bot, room.roomId);
+ const button = page.getByTestId("join-call-button");
+ await expect(button).toBeInViewport({ timeout: 5000 });
+ // And test joining
+ await button.click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ console.log(frameUrlStr);
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+
+ expect(hash.get("intent")).toEqual("join_existing_dm");
+ expect(hash.get("skipLobby")).toEqual(null);
+ });
+
+ [true, false].forEach((skipLobbyToggle) => {
+ test(
+ `should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
+ { tag: ["@screenshot"] },
+ async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ // Allow bob to create a call
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+ // Fake a start of a call
+ await sendRTCState(bot, room.roomId, "ring");
+ const toast = page.locator(".mx_Toast_toast");
+ const button = toast.getByRole("button", { name: "Join" });
+ if (skipLobbyToggle) {
+ await toast.getByRole("switch").check();
+ await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-checked.png");
+ } else {
+ await toast.getByRole("switch").uncheck();
+ await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-unchecked.png");
+ }
+
+ // And test joining
+ await button.click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ console.log(frameUrlStr);
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+
+ expect(hash.get("intent")).toEqual("join_existing_dm");
+ expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
+ },
+ );
+ });
+ });
+
+ test.describe("Video Rooms", () => {
+ test.use({
+ config: {
+ features: {
+ feature_video_rooms: true,
+ feature_element_call_video_rooms: true,
+ },
+ },
+ });
+ test("should be able to create and join a video room", async ({ page, user }) => {
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
+ await page.getByRole("menuitem", { name: "New video room" }).click();
+ await page.getByRole("textbox", { name: "Name" }).fill("Test room");
+ await page.getByRole("button", { name: "Create video room" }).click();
+ await expect(page).toHaveURL(new RegExp(`/#/room/`));
+ const roomId = new URL(page.url()).hash.slice("#/room/".length);
+
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ await expect(frameUrlStr).toBeDefined();
+ // Ensure we set the correct parameters for ECall.
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, { roomId });
+ expect(hash.get("intent")).toEqual("join_existing");
+ expect(hash.get("skipLobby")).toEqual("false");
+ expect(hash.get("returnToLobby")).toEqual("true");
+ });
+ });
+});
diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts
index 8296e9111e..86cb581397 100644
--- a/playwright/pages/client.ts
+++ b/playwright/pages/client.ts
@@ -469,6 +469,27 @@ export class Client {
);
}
+ /**
+ * Set a power level to one or multiple users.
+ * Will apply changes atop of current power level event.
+ * @param roomId - the room to update power levels in
+ * @param userId - the ID of the user or users to update power levels of
+ * @param powerLevel - the numeric power level to update given users to
+ */
+ public async setPowerLevel(
+ roomId: string,
+ userId: string | string[],
+ powerLevel: number,
+ ): Promise {
+ const client = await this.prepareClient();
+ return client.evaluate(
+ async (client, { roomId, userId, powerLevel }) => {
+ return client.setPowerLevel(roomId, userId, powerLevel);
+ },
+ { roomId, userId, powerLevel },
+ );
+ }
+
/**
* Leaves the given room.
* @param roomId ID of the room to leave
diff --git a/playwright/shared-component-snapshots/richlist-richitem--default-linux.png b/playwright/shared-component-snapshots/richlist-richitem--default-linux.png
new file mode 100644
index 0000000000..9a5ad9eb0f
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--default-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richitem--hover-linux.png b/playwright/shared-component-snapshots/richlist-richitem--hover-linux.png
new file mode 100644
index 0000000000..9a5ad9eb0f
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--hover-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richitem--selected-linux.png b/playwright/shared-component-snapshots/richlist-richitem--selected-linux.png
new file mode 100644
index 0000000000..f9c92b066f
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--selected-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richitem--separator-linux.png b/playwright/shared-component-snapshots/richlist-richitem--separator-linux.png
new file mode 100644
index 0000000000..a405f35902
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--separator-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richitem--without-timestamp-linux.png b/playwright/shared-component-snapshots/richlist-richitem--without-timestamp-linux.png
new file mode 100644
index 0000000000..de5ecda7c3
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--without-timestamp-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richlist--default-linux.png b/playwright/shared-component-snapshots/richlist-richlist--default-linux.png
new file mode 100644
index 0000000000..7919f356df
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richlist--default-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richlist--empty-linux.png b/playwright/shared-component-snapshots/richlist-richlist--empty-linux.png
new file mode 100644
index 0000000000..f655ecab96
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richlist--empty-linux.png differ
diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png
index 1a66050e5f..cdeb49f128 100644
Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png differ
diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png
index f463282be7..b481505235 100644
Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png differ
diff --git a/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png b/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png
index e18821b774..438691fd44 100644
Binary files a/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png and b/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png differ
diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png
index 4c7bc26cd2..01ebc9d415 100644
Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ
diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-checked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-checked-linux.png
new file mode 100644
index 0000000000..f309c37f3a
Binary files /dev/null and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-checked-linux.png differ
diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-unchecked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-unchecked-linux.png
new file mode 100644
index 0000000000..6d75547fcd
Binary files /dev/null and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-unchecked-linux.png differ
diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png
new file mode 100644
index 0000000000..aa181714ae
Binary files /dev/null and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png differ
diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png
new file mode 100644
index 0000000000..9893a76cb7
Binary files /dev/null and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png differ
diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts
index b367f1ca30..9180098ffa 100644
--- a/playwright/testcontainers/mas.ts
+++ b/playwright/testcontainers/mas.ts
@@ -10,7 +10,7 @@ import {
type StartedPostgreSqlContainer,
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
-const TAG = "main@sha256:a29fa92aca82fd4cdf6b84abaa14935f111f281f9bffeb30fdb8fe2353c0108c";
+const TAG = "main@sha256:09f64cd1633f1c82756b8e7d83cec4575b15782709674b0a69a4ad2a931e4e4f";
/**
* MatrixAuthenticationServiceContainer which freezes the docker digest to
diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts
index 477185a639..b234f9f389 100644
--- a/playwright/testcontainers/synapse.ts
+++ b/playwright/testcontainers/synapse.ts
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
-const TAG = "develop@sha256:40ef11b61d70bda94266324d159cc7d807e64b26ad03788e386d5084abb3c198";
+const TAG = "develop@sha256:0fd823705517826336ed5831093b1cf0b00f535884926cee994100dcddf15d1f";
/**
* SynapseContainer which freezes the docker digest to stabilise tests,
diff --git a/res/css/_common.pcss b/res/css/_common.pcss
index 3eed8c93c6..997848091d 100644
--- a/res/css/_common.pcss
+++ b/res/css/_common.pcss
@@ -602,6 +602,7 @@ legend {
.mx_AccessibleButton,
.mx_IdentityServerPicker button,
.mx_AccessSecretStorageDialog button,
+ .mx_InviteDialog_section button,
[class|="maplibregl"]
),
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
@@ -643,7 +644,8 @@ legend {
.mx_ThemeChoicePanel_CustomTheme button,
.mx_UnpinAllDialog button,
.mx_ShareDialog button,
- .mx_EncryptionUserSettingsTab button
+ .mx_EncryptionUserSettingsTab button,
+ .mx_InviteDialog_section button
):focus,
.mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):focus,
diff --git a/res/css/views/dialogs/_InviteDialog.pcss b/res/css/views/dialogs/_InviteDialog.pcss
index 0f952049cf..571b1ad506 100644
--- a/res/css/views/dialogs/_InviteDialog.pcss
+++ b/res/css/views/dialogs/_InviteDialog.pcss
@@ -68,21 +68,6 @@ Please see LICENSE files in the repository root for full details.
.mx_InviteDialog_section {
padding-bottom: $spacing-4;
- h3 {
- font-size: $font-12px;
- color: $muted-fg-color;
- font-weight: bold;
- text-transform: uppercase;
- }
-
- > p {
- margin: 0;
- }
-
- > span {
- color: $primary-content;
- }
-
.mx_InviteDialog_section_showMore {
margin: 7px 18px;
display: block;
@@ -194,10 +179,13 @@ Please see LICENSE files in the repository root for full details.
.mx_InviteDialog_userSections {
flex-grow: 1;
padding-inline-end: 0;
+ display: flex;
+ flex-direction: column;
+ margin-top: var(--cpd-space-3x);
+ gap: var(--cpd-space-3x);
.mx_InviteDialog_section {
padding-bottom: 0;
- margin-top: $spacing-12;
}
}
}
@@ -249,7 +237,6 @@ Please see LICENSE files in the repository root for full details.
}
.mx_InviteDialog_userSections {
- margin-top: $spacing-4;
overflow-y: auto;
padding: 0 45px $spacing-4 0;
}
@@ -325,48 +312,6 @@ Please see LICENSE files in the repository root for full details.
gap: $spacing-8 $spacing-12;
align-items: center;
- &.mx_InviteDialog_tile--room {
- /* mx_InviteDialog_tile_avatarStack, mx_InviteDialog_tile_nameStack, time */
- grid-template-columns: min-content auto auto;
- padding: $spacing-4 $spacing-8;
-
- &:hover {
- background-color: $header-panel-bg-color;
- border-radius: 4px;
- }
-
- .mx_InviteDialog_tile--room_selected {
- border-radius: 36px;
- background-color: var(--cpd-color-bg-success-subtle);
-
- &::before {
- content: "";
- width: 24px;
- height: 24px;
- grid-column: 1;
- grid-row: 1;
- mask-image: url("@vector-im/compound-design-tokens/icons/check.svg");
- mask-size: 100%;
- mask-repeat: no-repeat;
- position: absolute;
- top: 6px; /* 50% */
- left: 6px; /* 50% */
- background-color: $primary-content;
- }
- }
-
- .mx_InviteDialog_tile--room_time {
- margin-inline-start: auto;
- width: max-content;
- font-size: $font-12px;
- color: $muted-fg-color;
- }
-
- .mx_InviteDialog_tile--room_highlight {
- font-weight: 900;
- }
- }
-
&.mx_InviteDialog_tile--inviterError {
grid-template-columns: max-content auto; /* max-content = avatar width */
margin-bottom: $spacing-24;
@@ -388,15 +333,11 @@ Please see LICENSE files in the repository root for full details.
vertical-align: middle;
}
- .mx_InviteDialog_tile_avatarStack,
- .mx_InviteDialog_tile--room_selected {
+ .mx_InviteDialog_tile_avatarStack {
width: 36px;
height: 36px;
display: inline-block;
position: relative;
- }
-
- .mx_InviteDialog_tile_avatarStack {
grid-row-start: 1;
grid-column-start: 1;
diff --git a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
index f8fc31ae12..378f2e75da 100644
--- a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
+++ b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
@@ -12,10 +12,7 @@
display: none;
}
- ul {
- margin: unset;
- padding: unset;
- list-style-type: none;
+ .mx_RoomListPrimaryFilters_list {
/**
* The InteractionObserver needs the height to be set to work properly.
*/
diff --git a/res/media/message.mp3 b/res/media/message.mp3
index 0e2f3207a7..5e9645adaf 100644
Binary files a/res/media/message.mp3 and b/res/media/message.mp3 differ
diff --git a/res/media/message.ogg b/res/media/message.ogg
index 23e5a89f1c..d5efbd27a3 100644
Binary files a/res/media/message.ogg and b/res/media/message.ogg differ
diff --git a/res/media/ring.mp3 b/res/media/ring.mp3
index 9b08ec7b36..386cce6998 100644
Binary files a/res/media/ring.mp3 and b/res/media/ring.mp3 differ
diff --git a/res/media/ring.ogg b/res/media/ring.ogg
index 41a22160b0..48c3f1c2ad 100644
Binary files a/res/media/ring.ogg and b/res/media/ring.ogg differ
diff --git a/scripts/get-version-from-git.sh b/scripts/get-version-from-git.sh
index c8459dba44..90de770478 100755
--- a/scripts/get-version-from-git.sh
+++ b/scripts/get-version-from-git.sh
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
-# Echoes a version based on the git hashes of the element-web, react-sdk & js-sdk checkouts, for the case where
+# Echoes a version based on the git hashes of the element-web & js-sdk checkouts, for the case where
# these dependencies are git checkouts.
set -e
-# Since the deps are fetched from git, we can rev-parse
+# Since the deps are fetched from git & linked, we can rev-parse
JSSDK_SHA=$(git -C node_modules/matrix-js-sdk rev-parse --short=12 HEAD)
VECTOR_SHA=$(git rev-parse --short=12 HEAD) # use the ACTUAL SHA rather than assume develop
-echo $VECTOR_SHA-js-$JSSDK_SHA
+echo "$VECTOR_SHA-js-$JSSDK_SHA"
diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index 05dd437f94..21641aec09 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -508,4 +508,19 @@ export default abstract class BasePlatform {
* Begin update polling, if applicable
*/
public startUpdater(): void {}
+
+ /**
+ * Checks if the current session is lock-free, i.e., no other instance is holding the session lock.
+ * Platforms that support session locking should override this method.
+ * @returns {boolean} True if the session is lock-free, false otherwise.
+ */
+ public abstract checkSessionLockFree(): boolean;
+ /**
+ * Attempts to acquire a session lock for this instance.
+ * If another instance is detected, calls the provided callback.
+ * Platforms that support session locking should override this method.
+ * @param _onNewInstance Callback to invoke if a new instance is detected.
+ * @returns {Promise} True if the lock was acquired, false otherwise.
+ */
+ public abstract getSessionLock(_onNewInstance: () => Promise): Promise;
}
diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx
index e618fa62a0..33770fe4bf 100644
--- a/src/LegacyCallHandler.tsx
+++ b/src/LegacyCallHandler.tsx
@@ -390,6 +390,9 @@ export default class LegacyCallHandler extends TypedEventEmitter {
private async initSession(): Promise {
// The Rust Crypto SDK will break if two Element instances try to use the same datastore at once, so
// make sure we are the only Element instance in town (on this browser/domain).
- if (!(await getSessionLock(() => this.onSessionLockStolen()))) {
+ const platform = PlatformPeg.get();
+ if (platform && !(await platform.getSessionLock(() => this.onSessionLockStolen()))) {
// we failed to get the lock. onSessionLockStolen should already have been called, so nothing left to do.
return;
}
@@ -479,7 +479,8 @@ export default class MatrixChat extends React.PureComponent {
// mounted.
if (!this.sessionLoadStarted) {
this.sessionLoadStarted = true;
- if (!checkSessionLockFree()) {
+ const platform = PlatformPeg.get();
+ if (platform && !platform.checkSessionLockFree()) {
// another instance holds the lock; confirm its theft before proceeding
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
} else {
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 31159e56e2..7233df1cae 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -2620,7 +2620,6 @@ export class RoomView extends React.Component {
diff --git a/src/components/views/beacon/BeaconListItem.tsx b/src/components/views/beacon/BeaconListItem.tsx
index ceedbf05fd..b41916efd8 100644
--- a/src/components/views/beacon/BeaconListItem.tsx
+++ b/src/components/views/beacon/BeaconListItem.tsx
@@ -11,7 +11,6 @@ import { type Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/m
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
-import { humanizeTime } from "../../../utils/humanize";
import { preventDefaultWrapper } from "../../../utils/NativeEventUtils";
import { _t } from "../../../languageHandler";
import MemberAvatar from "../avatars/MemberAvatar";
@@ -19,6 +18,7 @@ import BeaconStatus from "./BeaconStatus";
import { BeaconDisplayStatus } from "./displayStatus";
import StyledLiveBeaconIcon from "./StyledLiveBeaconIcon";
import ShareLatestLocation from "./ShareLatestLocation";
+import { humanizeTime } from "../../../shared-components/utils/humanize";
interface Props {
beacon: Beacon;
diff --git a/src/components/views/beacon/RoomCallBanner.tsx b/src/components/views/beacon/RoomCallBanner.tsx
index e4f0dfa608..7c2131d4b4 100644
--- a/src/components/views/beacon/RoomCallBanner.tsx
+++ b/src/components/views/beacon/RoomCallBanner.tsx
@@ -35,7 +35,7 @@ const RoomCallBannerInner: React.FC = ({ roomId, call }) =>
action: Action.ViewRoom,
room_id: roomId,
view_call: true,
- skipLobby: "shiftKey" in ev ? ev.shiftKey : false,
+ skipLobby: ("shiftKey" in ev && ev.shiftKey) || undefined,
metricsTrigger: undefined,
});
},
diff --git a/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx b/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx
index 49e7cad17b..d6a5f79aeb 100644
--- a/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx
+++ b/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx
@@ -17,6 +17,7 @@ import { EncryptionCardButtons } from "../settings/encryption/EncryptionCardButt
import { type OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserTab";
+import SdkConfig from "../../../SdkConfig";
interface Props {
onFinished: (dismissed: boolean) => void;
@@ -60,7 +61,7 @@ export default class ConfirmKeyStorageOffDialog extends React.Component {
a: (sub) => (
<>
-
+
{sub}
>
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 73049122dc..bc0ca71d4d 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -24,7 +24,6 @@ import { getDefaultIdentityServerUrl, setToDefaultIdentityServer } from "../../.
import { buildActivityScores, buildMemberScores, compareMembers } from "../../../utils/SortMembers";
import { abbreviateUrl } from "../../../utils/UrlUtils";
import IdentityAuthClient from "../../../IdentityAuthClient";
-import { humanizeTime } from "../../../utils/humanize";
import { type IInviteResult, inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite";
import { Action } from "../../../dispatcher/actions";
import { DefaultTagID } from "../../../stores/room-list/models";
@@ -65,6 +64,8 @@ import AskInviteAnywayDialog, { type UnknownProfiles } from "./AskInviteAnywayDi
import { SdkContextClass } from "../../../contexts/SDKContext";
import { type UserProfilesStore } from "../../../stores/UserProfilesStore";
import InviteProgressBody from "./InviteProgressBody.tsx";
+import { RichList } from "../../../shared-components/rich-list/RichList";
+import { RichItem } from "../../../shared-components/rich-list/RichItem";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@@ -163,7 +164,6 @@ interface IDMRoomTileProps {
member: Member;
lastActiveTs?: number;
onToggle(member: Member): void;
- highlightWord: string;
isSelected: boolean;
}
@@ -176,54 +176,8 @@ class DMRoomTile extends React.PureComponent {
this.props.onToggle(this.props.member);
};
- private highlightName(str: string): ReactNode {
- if (!this.props.highlightWord) return str;
-
- // We convert things to lowercase for index searching, but pull substrings from
- // the submitted text to preserve case. Note: we don't need to htmlEntities the
- // string because React will safely encode the text for us.
- const lowerStr = str.toLowerCase();
- const filterStr = this.props.highlightWord.toLowerCase();
-
- const result: JSX.Element[] = [];
-
- let i = 0;
- let ii: number;
- while ((ii = lowerStr.indexOf(filterStr, i)) >= 0) {
- // Push any text we missed (first bit/middle of text)
- if (ii > i) {
- // Push any text we aren't highlighting (middle of text match, or beginning of text)
- result.push({str.substring(i, ii)});
- }
-
- i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching)
-
- // Highlight the word the user entered
- const substr = str.substring(i, filterStr.length + i);
- result.push(
-
- {substr}
- ,
- );
- i += substr.length;
- }
-
- // Push any text we missed (end of text)
- if (i < str.length) {
- result.push({str.substring(i)});
- }
-
- return result;
- }
-
public render(): React.ReactNode {
- let timestamp: JSX.Element | undefined;
- if (this.props.lastActiveTs) {
- const humanTs = humanizeTime(this.props.lastActiveTs);
- timestamp = {humanTs};
- }
-
- const avatarSize = "36px";
+ const avatarSize = "32px";
const avatar = (this.props.member as ThreepidMember).isEmail ? (
) : (
@@ -241,40 +195,23 @@ class DMRoomTile extends React.PureComponent {
/>
);
- let checkmark: JSX.Element | undefined;
- if (this.props.isSelected) {
- // To reduce flickering we put the 'selected' room tile above the real avatar
- checkmark = ;
- }
-
- // To reduce flickering we put the checkmark on top of the actual avatar (prevents
- // the browser from reloading the image source when the avatar remounts).
- const stackedAvatar = (
-
- {avatar}
- {checkmark}
-
- );
-
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(this.props.member.userId, {
withDisplayName: true,
});
const caption = (this.props.member as ThreepidMember).isEmail
? _t("invite|email_caption")
- : this.highlightName(userIdentifier || this.props.member.userId);
+ : userIdentifier || this.props.member.userId;
return (
-
- {stackedAvatar}
-
-