Compare commits

..

11 Commits

Author SHA1 Message Date
Timo
87f02b28a5 Merge pull request #3293 from element-hq/toger5/more-disconnect-logging (#3297)
Fix creating two lk rooms if there is no local store setup (fixes a resulting disconnect bug)
2025-05-28 09:07:50 +02:00
Timo
24cb61c24b Merge pull request #3291 from element-hq:toger5/backport-disable-device-switching-and-update-state
Backport: Disable device switching when in controlled audio devices mode
2025-05-23 17:56:49 +02:00
Timo
8f424452fc Disable device switching when in controlled audio devices mode (#3290)
* Disable device switching when in controlled audio devices mode

* Temporarily switch matrix-js-sdk to robin/embedded-no-update-state

To allow us to test this change on Element X, which does not yet support the update_state action.

* Also add a check for controlled audio devices in useAudioContext

* use develop branch

* fix tests

---------

Co-authored-by: Robin <robin@robin.town>
2025-05-23 17:55:28 +02:00
Robin
c2ce1fd382 Merge pull request #3288 from element-hq/robin/backport-audio-controls
Audio device controls for mobile native audio device selection
2025-05-22 14:32:12 -04:00
Robin
52895ed599 Audio device controls for mobile native audio device selection
Backport of 0971a15c40.
2025-05-22 14:11:46 -04:00
Robin
0719320ceb Merge pull request #3286 from element-hq/robin/backport-reset-develop
Reset to develop branch of matrix-js-sdk
2025-05-22 13:57:06 -04:00
Robin
593a50289a Reset to develop branch of matrix-js-sdk
Now that the toger5/add-room-key-fallback-on-encryption-manager-not-supported branch has been merged, we can reset to develop. (And need to, actually, because that branch is deleted.)
2025-05-22 13:12:23 -04:00
Robin
60881d7b11 Merge pull request #3281 from element-hq/robin/backport-reintroduce-update-state
Improve the reliability of state changes in widget mode
2025-05-21 14:36:24 -04:00
Robin
5f65c51cf3 Reference matrix-js-sdk by branch name
Rather than by commit, which makes it hard to tell whether we're using mainline matrix-js-sdk or not.
2025-05-21 14:27:15 -04:00
Robin
4876bddea4 Update matrix-widget-api to support update_state action 2025-05-21 14:27:15 -04:00
Robin
a5a737f830 Update matrix-js-sdk to support update_state action 2025-05-21 14:27:15 -04:00
49 changed files with 2147 additions and 2943 deletions

View File

@@ -1,7 +1,7 @@
name: Prevent blocked
on:
pull_request_target:
types: [opened, labeled, unlabeled, synchronize]
types: [opened, labeled, unlabeled]
jobs:
prevent-blocked:
name: Prevent blocked

View File

@@ -9,11 +9,6 @@ on:
type: string # This would ideally be a `choice` type, but that isn't supported yet
description: The package type to be built. Must be one of 'full' or 'embedded'
required: true
build_mode:
type: string # This would ideally be a `choice` type, but that isn't supported yet
description: The build mode for vite. Must be either 'development' or 'production'
required: false
default: production
secrets:
SENTRY_ORG:
required: true
@@ -42,8 +37,20 @@ jobs:
node-version-file: ".node-version"
- name: Install dependencies
run: "yarn install --immutable"
- name: Build Element Call
run: ${{ format('yarn run build:{0}:{1}', inputs.package, inputs.build_mode) }}
- name: Build full version
if: ${{ inputs.package == 'full' }}
run: "yarn run build:full"
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
VITE_APP_VERSION: ${{ inputs.vite_app_version }}
NODE_OPTIONS: "--max-old-space-size=4096"
- name: Build embedded
if: ${{ inputs.package == 'embedded' }}
run: "yarn run build:embedded"
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

View File

@@ -14,7 +14,6 @@ jobs:
with:
package: full
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
build_mode: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'development build') && 'development' || 'production' }}
secrets:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
@@ -62,7 +61,6 @@ jobs:
with:
package: embedded
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
build_mode: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'development build') && 'development' || 'production' }}
secrets:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

View File

@@ -46,8 +46,7 @@ jobs:
run: yarn playwright install --with-deps
- name: Run backend components
run: |
docker compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml pull
docker compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml up -d
docker compose -f playwright-backend-docker-compose.yml up -d
docker ps
- name: Copy config file
run: cp config/config.devenv.json public/config.json

View File

@@ -43,11 +43,6 @@ server {
# MatrixRTC reverse proxy
# - MatrixRTC Authorization Service
# - LiveKit SFU websocket signaling connection
upstream jwt-auth-services {
server auth-server:6080;
server host.docker.internal:6080;
}
server {
listen 80;
listen [::]:80;
@@ -67,9 +62,8 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# JWT Service running at port 6080
proxy_pass http://jwt-auth-services/;
# JWT Service running at port 8080
proxy_pass http://auth-server:8080/;
}
location ^~ /livekit/sfu/ {

View File

@@ -6,7 +6,7 @@ services:
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
hostname: auth-server
environment:
- LIVEKIT_JWT_PORT=6080
- LK_JWT_PORT=8080
- LIVEKIT_URL=wss://matrix-rtc.m.localhost/livekit/sfu
- LIVEKIT_KEY=devkey
- LIVEKIT_SECRET=secret
@@ -18,7 +18,7 @@ services:
condition: on-failure
ports:
# HOST_PORT:CONTAINER_PORT
- 6080:6080
- 8080:8080
networks:
- ecbackend

View File

@@ -2,7 +2,7 @@
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
[versions]
android_gradle_plugin = "8.10.1"
android_gradle_plugin = "8.10.0"
[libraries]
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@@ -8,11 +8,7 @@
"dev:embedded": "vite --config vite-embedded.config.js",
"build": "yarn build:full",
"build:full": "NODE_OPTIONS=--max-old-space-size=16384 vite build",
"build:full:production": "yarn build:full",
"build:full:development": "yarn build:full --mode development",
"build:embedded": "yarn build:full --config vite-embedded.config.js",
"build:embedded:production": "yarn build:embedded",
"build:embedded:development": "yarn build:embedded --mode development",
"serve": "vite preview",
"prettier:check": "prettier -c .",
"prettier:format": "prettier -w .",
@@ -26,7 +22,6 @@
"test": "vitest",
"test:coverage": "vitest --coverage",
"backend": "docker-compose -f dev-backend-docker-compose.yml up",
"backend-playwright": "docker-compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml up",
"test:playwright": "playwright test",
"test:playwright:open": "yarn test:playwright --ui",
"links:enable": "mv .links.disabled.yaml .links.yaml & touch .links.yaml",
@@ -48,11 +43,11 @@
"@livekit/track-processors": "^0.5.5",
"@mediapipe/tasks-vision": "^0.10.18",
"@opentelemetry/api": "^1.4.0",
"@opentelemetry/core": "^2.0.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.201.0",
"@opentelemetry/resources": "^2.0.0",
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@opentelemetry/sdk-trace-web": "^2.0.0",
"@opentelemetry/core": "^1.25.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
"@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-trace-base": "^1.25.1",
"@opentelemetry/sdk-trace-web": "^1.9.1",
"@opentelemetry/semantic-conventions": "^1.25.1",
"@playwright/test": "^1.52.0",
"@radix-ui/react-dialog": "^1.0.4",
@@ -81,8 +76,8 @@
"@typescript-eslint/eslint-plugin": "^8.31.0",
"@typescript-eslint/parser": "^8.31.0",
"@use-gesture/react": "^10.2.11",
"@vector-im/compound-design-tokens": "^4.0.0",
"@vector-im/compound-web": "^7.12.0",
"@vector-im/compound-design-tokens": "^3.0.0",
"@vector-im/compound-web": "^7.2.0",
"@vitejs/plugin-react": "^4.0.1",
"@vitest/coverage-v8": "^3.0.0",
"babel-plugin-transform-vite-meta-env": "^1.0.3",
@@ -93,7 +88,7 @@
"eslint-plugin-deprecate": "^0.8.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "2.1.0",
"eslint-plugin-matrix-org": "^2.0.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-rxjs": "^5.0.3",
@@ -130,7 +125,7 @@
"unique-names-generator": "^4.6.0",
"vaul": "^1.0.0",
"vite": "^6.0.0",
"vite-plugin-generate-file": "^0.3.0",
"vite-plugin-generate-file": "^0.2.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-svgr": "^4.0.0",
"vitest": "^3.0.0",

View File

@@ -5,13 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
type Browser,
type Page,
test,
expect,
type JSHandle,
} from "@playwright/test";
import { type Page, test, expect, type JSHandle } from "@playwright/test";
import type { MatrixClient } from "matrix-js-sdk";
@@ -36,8 +30,8 @@ const PASSWORD = "foobarbaz1!";
const CONFIG_JSON = {
default_server_config: {
"m.homeserver": {
base_url: "https://synapse.m.localhost",
server_name: "synapse.m.localhost",
base_url: "http://synapse.localhost:8008",
server_name: "synapse.localhost",
},
},
@@ -80,52 +74,6 @@ async function setDevToolElementCallDevUrl(page: Page): Promise<void> {
});
}
/**
* Registers a new user and returns page, clientHandle and mxId.
*/
async function registerUser(
browser: Browser,
username: string,
): Promise<{ page: Page; clientHandle: JSHandle<MatrixClient>; mxId: string }> {
const userContext = await browser.newContext({
reducedMotion: "reduce",
});
const page = await userContext.newPage();
await page.goto("http://localhost:8081/#/welcome");
await page.getByRole("link", { name: "Create Account" }).click();
await page.getByRole("textbox", { name: "Username" }).fill(username);
await page
.getByRole("textbox", { name: "Password", exact: true })
.fill(PASSWORD);
await page.getByRole("textbox", { name: "Confirm password" }).click();
await page.getByRole("textbox", { name: "Confirm password" }).fill(PASSWORD);
await page.getByRole("button", { name: "Register" }).click();
const continueButton = page.getByRole("button", { name: "Continue" });
try {
await expect(continueButton).toBeVisible({ timeout: 5000 });
await page
.getByRole("textbox", { name: "Password", exact: true })
.fill(PASSWORD);
await continueButton.click();
} catch {
// continueButton not visible, continue as normal
}
await expect(
page.getByRole("heading", { name: `Welcome ${username}` }),
).toBeVisible();
await setDevToolElementCallDevUrl(page);
const clientHandle = await page.evaluateHandle(() =>
window.mxMatrixClientPeg.get(),
);
const mxId = (await clientHandle.evaluate(
(cli: MatrixClient) => cli.getUserId(),
clientHandle,
))!;
return { page, clientHandle, mxId };
}
export const widgetTest = test.extend<MyFixtures>({
asWidget: async ({ browser, context }, pUse) => {
await context.route(`http://localhost:8081/config.json*`, async (route) => {
@@ -135,17 +83,61 @@ export const widgetTest = test.extend<MyFixtures>({
const userA = `brooks_${Date.now()}`;
const userB = `whistler_${Date.now()}`;
// Register users
const {
page: ewPage1,
clientHandle: brooksClientHandle,
mxId: brooksMxId,
} = await registerUser(browser, userA);
const {
page: ewPage2,
clientHandle: whistlerClientHandle,
mxId: whistlerMxId,
} = await registerUser(browser, userB);
const user1Context = await browser.newContext({
reducedMotion: "reduce",
});
const ewPage1 = await user1Context.newPage();
// Register the first user
await ewPage1.goto("http://localhost:8081/#/welcome");
await ewPage1.getByRole("link", { name: "Create Account" }).click();
await ewPage1.getByRole("textbox", { name: "Username" }).fill(userA);
await ewPage1
.getByRole("textbox", { name: "Password", exact: true })
.fill(PASSWORD);
await ewPage1.getByRole("textbox", { name: "Confirm password" }).click();
await ewPage1
.getByRole("textbox", { name: "Confirm password" })
.fill(PASSWORD);
await ewPage1.getByRole("button", { name: "Register" }).click();
await expect(
ewPage1.getByRole("heading", { name: `Welcome ${userA}` }),
).toBeVisible();
await setDevToolElementCallDevUrl(ewPage1);
const brooksClientHandle = await ewPage1.evaluateHandle(() =>
window.mxMatrixClientPeg.get(),
);
const brooksMxId = (await brooksClientHandle.evaluate((cli) => {
return cli.getUserId();
}, brooksClientHandle))!;
const user2Context = await browser.newContext({
reducedMotion: "reduce",
});
const ewPage2 = await user2Context.newPage();
// Register the second user
await ewPage2.goto("http://localhost:8081/#/welcome");
await ewPage2.getByRole("link", { name: "Create Account" }).click();
await ewPage2.getByRole("textbox", { name: "Username" }).fill(userB);
await ewPage2
.getByRole("textbox", { name: "Password", exact: true })
.fill(PASSWORD);
await ewPage2.getByRole("textbox", { name: "Confirm password" }).click();
await ewPage2
.getByRole("textbox", { name: "Confirm password" })
.fill(PASSWORD);
await ewPage2.getByRole("button", { name: "Register" }).click();
await expect(
ewPage2.getByRole("heading", { name: `Welcome ${userB}` }),
).toBeVisible();
await setDevToolElementCallDevUrl(ewPage2);
const whistlerClientHandle = await ewPage2.evaluateHandle(() =>
window.mxMatrixClientPeg.get(),
);
const whistlerMxId = (await whistlerClientHandle.evaluate((cli) => {
return cli.getUserId();
}, whistlerClientHandle))!;
// Invite the second user
await ewPage1.getByRole("button", { name: "Add room" }).click();

View File

@@ -9,14 +9,11 @@ import { expect, test } from "@playwright/test";
import { widgetTest } from "../fixtures/widget-user.ts";
// Skip test, including Fixtures
widgetTest.skip(
({ browserName }) => browserName === "firefox",
"This test is not working on firefox, after hangup brooks is locked in a strange state with a blank widget",
);
widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => {
test.slow(); // Triples the timeout
test.skip(
browserName === "firefox",
"This test is not working on firefox, after hangup brooks is locked in a strange state with a blank widget",
);
const { brooks, whistler } = asWidget;

View File

@@ -59,7 +59,7 @@
}
],
"semanticCommits": "disabled",
"ignoreDeps": ["posthog-js", "eslint-plugin-matrix-org"],
"ignoreDeps": ["posthog-js"],
"vulnerabilityAlerts": {
"schedule": ["at any time"],
"prHourlyLimit": 0,

View File

@@ -10,7 +10,7 @@ import classNames from "classnames";
import { useTranslation } from "react-i18next";
import * as Sentry from "@sentry/react";
import { logger } from "matrix-js-sdk/lib/logger";
import { ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import styles from "./FullScreenView.module.css";
@@ -67,7 +67,7 @@ export const ErrorPage = ({ error, widget }: ErrorPageProps): ReactElement => {
) : (
<ErrorView
widget={widget}
Icon={ErrorSolidIcon}
Icon={ErrorIcon}
title={t("error.generic")}
rageshake
fatal

View File

@@ -3,7 +3,7 @@
exports[`the content is rendered when the modal is open 1`] = `
<div
aria-labelledby="radix-:r4:"
class="overlay animate modal dialog _glass_sepwu_8"
class="overlay animate modal dialog _glass_1x9g9_17"
data-state="open"
id="radix-:r3:"
role="dialog"
@@ -17,7 +17,7 @@ exports[`the content is rendered when the modal is open 1`] = `
class="header"
>
<h2
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
id="radix-:r4:"
>
My modal

View File

@@ -12,7 +12,7 @@ exports[`Toast > renders 1`] = `
type="button"
>
<h3
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36"
class="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45"
id="radix-:r4:"
>
Hello world!

View File

@@ -107,13 +107,13 @@ export class RageshakeSpanProcessor implements SpanProcessor {
startTime,
duration,
references:
span.parentSpanContext?.spanId === undefined
span.parentSpanId === undefined
? []
: [
{
refType: "CHILD_OF",
traceID: traceId,
spanID: span.parentSpanContext?.spanId,
spanID: span.parentSpanId,
},
],
tags: dumpAttributes(span.attributes),

View File

@@ -10,7 +10,7 @@ exports[`Can close reaction dialog 1`] = `
aria-expanded="true"
aria-haspopup="true"
aria-labelledby=":rb5:"
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
role="button"
@@ -26,7 +26,7 @@ exports[`Can close reaction dialog 1`] = `
>
<path
clip-rule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m3.536-6.464a1 1 0 0 0-1.415-1.415A3 3 0 0 1 12 15a3 3 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A5 5 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464M10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
fill-rule="evenodd"
/>
</svg>
@@ -44,7 +44,7 @@ exports[`Can fully expand emoji picker 1`] = `
aria-expanded="true"
aria-haspopup="true"
aria-labelledby=":r7m:"
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
role="button"
@@ -60,7 +60,7 @@ exports[`Can fully expand emoji picker 1`] = `
>
<path
clip-rule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m3.536-6.464a1 1 0 0 0-1.415-1.415A3 3 0 0 1 12 15a3 3 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A5 5 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464M10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
fill-rule="evenodd"
/>
</svg>
@@ -75,7 +75,7 @@ exports[`Can lower hand 1`] = `
aria-expanded="false"
aria-haspopup="true"
aria-labelledby=":r36:"
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary"
data-size="lg"
role="button"
@@ -91,7 +91,7 @@ exports[`Can lower hand 1`] = `
>
<path
clip-rule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m3.536-6.464a1 1 0 0 0-1.415-1.415A3 3 0 0 1 12 15a3 3 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A5 5 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464M10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
fill-rule="evenodd"
/>
</svg>
@@ -109,7 +109,7 @@ exports[`Can open menu 1`] = `
aria-expanded="true"
aria-haspopup="true"
aria-labelledby=":r0:"
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
role="button"
@@ -125,7 +125,7 @@ exports[`Can open menu 1`] = `
>
<path
clip-rule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m3.536-6.464a1 1 0 0 0-1.415-1.415A3 3 0 0 1 12 15a3 3 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A5 5 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464M10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm3.536-6.464a1 1 0 0 0-1.415-1.415A2.988 2.988 0 0 1 12 15a2.988 2.988 0 0 1-2.121-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
fill-rule="evenodd"
/>
</svg>
@@ -140,7 +140,7 @@ exports[`Can raise hand 1`] = `
aria-expanded="false"
aria-haspopup="true"
aria-labelledby=":r1j:"
class="_button_vczzf_8 raisedButton _has-icon_vczzf_57 _icon-only_vczzf_50"
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
role="button"
@@ -155,7 +155,7 @@ exports[`Can raise hand 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 3a1 1 0 1 1 2 0v8.5a.5.5 0 0 0 1 0V4a1 1 0 1 1 2 0v10.2l3.284-2.597a1.081 1.081 0 0 1 1.47 1.577c-.613.673-1.214 1.367-1.818 2.064-1.267 1.463-2.541 2.934-3.943 4.235A6 6 0 0 1 5 15V7a1 1 0 0 1 1.999 0v5.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 2 0v7.5a.5.5 0 0 0 1 0z"
d="M11 3a1 1 0 1 1 2 0v8.5a.5.5 0 0 0 1 0V4a1 1 0 1 1 2 0v10.2l3.284-2.597a1.081 1.081 0 0 1 1.47 1.577c-.613.673-1.214 1.367-1.818 2.064-1.267 1.463-2.541 2.934-3.944 4.235A6 6 0 0 1 5 15V7a1 1 0 0 1 2 0v5.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 2 0v7.5a.5.5 0 0 0 1 0V3Z"
/>
</svg>
</button>

View File

@@ -6,70 +6,58 @@ Please see LICENSE in the repository root for full details.
*/
import { useEffect, useMemo } from "react";
import { logger } from "matrix-js-sdk/lib/logger";
import {
setLocalStorageItemReactive,
useLocalStorage,
} from "../useLocalStorage";
import { getUrlParams } from "../UrlParams";
import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage";
import { type UrlParams, getUrlParams, useUrlParams } from "../UrlParams";
import { E2eeType } from "./e2eeType";
import { useClient } from "../ClientContext";
/**
* This setter will update the state for all `useRoomSharedKey` hooks
* if the password is different from the one in local storage or if its not yet in the local storage.
*/
export function saveKeyForRoom(roomId: string, password: string): void {
if (
localStorage.getItem(getRoomSharedKeyLocalStorageKey(roomId)) !== password
)
setLocalStorageItemReactive(
getRoomSharedKeyLocalStorageKey(roomId),
password,
);
setLocalStorageItem(getRoomSharedKeyLocalStorageKey(roomId), password);
}
const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
`room-shared-key-${roomId}`;
/**
* An upto-date shared key for the room. Either from local storage or the value from `setInitialValue`.
* @param roomId
* @param setInitialValue The value we get from the URL. The hook will overwrite the local storage value with this.
* @returns [roomSharedKey, setRoomSharedKey] like a react useState hook.
*/
const useRoomSharedKey = (
roomId: string,
setInitialValue?: string,
): [string | null, setKey: (key: string) => void] => {
const [roomSharedKey, setRoomSharedKey] = useLocalStorage(
getRoomSharedKeyLocalStorageKey(roomId),
);
useEffect(() => {
// If setInitialValue is available, update the local storage (usually the password from the url).
// This will update roomSharedKey but wont update the returned value since
// that already defaults to setInitialValue.
if (setInitialValue) setRoomSharedKey(setInitialValue);
}, [setInitialValue, setRoomSharedKey]);
const useInternalRoomSharedKey = (roomId: string): string | null => {
const key = getRoomSharedKeyLocalStorageKey(roomId);
const [roomSharedKey] = useLocalStorage(key);
// make sure we never return the initial null value from `useLocalStorage`
return [setInitialValue ?? roomSharedKey, setRoomSharedKey];
return roomSharedKey;
};
export function getKeyForRoom(roomId: string): string | null {
const { roomId: urlRoomId, password } = getUrlParams();
if (roomId !== urlRoomId)
logger.warn(
"requested key for a roomId which is not the current call room id (from the URL)",
roomId,
urlRoomId,
);
return (
password ?? localStorage.getItem(getRoomSharedKeyLocalStorageKey(roomId))
);
saveKeyFromUrlParams(getUrlParams());
const key = getRoomSharedKeyLocalStorageKey(roomId);
return localStorage.getItem(key);
}
function saveKeyFromUrlParams(urlParams: UrlParams): void {
if (!urlParams.password || !urlParams.roomId) return;
// Take the key from the URL and save it.
// It's important to always use the room ID specified in the URL
// when saving keys rather than whatever the current room ID might be,
// in case we've moved to a different room but the URL hasn't changed.
saveKeyForRoom(urlParams.roomId, urlParams.password);
}
/**
* Extracts the room password from the URL if one is present, saving it in localstorage
* and returning it in a tuple with the corresponding room ID from the URL.
* @returns A tuple of the roomId and password from the URL if the URL has both,
* otherwise [undefined, undefined]
*/
const useKeyFromUrl = (): [string, string] | [undefined, undefined] => {
const urlParams = useUrlParams();
useEffect(() => saveKeyFromUrlParams(urlParams), [urlParams]);
return urlParams.roomId && urlParams.password
? [urlParams.roomId, urlParams.password]
: [undefined, undefined];
};
export type Unencrypted = { kind: E2eeType.NONE };
export type SharedSecret = { kind: E2eeType.SHARED_KEY; secret: string };
export type PerParticipantE2EE = { kind: E2eeType.PER_PARTICIPANT };
@@ -78,11 +66,12 @@ export type EncryptionSystem = Unencrypted | SharedSecret | PerParticipantE2EE;
export function useRoomEncryptionSystem(roomId: string): EncryptionSystem {
const { client } = useClient();
const [storedPassword] = useRoomSharedKey(
getRoomSharedKeyLocalStorageKey(roomId),
getKeyForRoom(roomId) ?? undefined,
);
// make sure we've extracted the key from the URL first
// (and we still need to take the value it returns because
// the effect won't run in time for it to save to localstorage in
// time for us to read it out again).
const [urlRoomId, passwordFromUrl] = useKeyFromUrl();
const storedPassword = useInternalRoomSharedKey(roomId);
const room = client?.getRoom(roomId);
const e2eeSystem = <EncryptionSystem>useMemo(() => {
if (!room) return { kind: E2eeType.NONE };
@@ -91,10 +80,15 @@ export function useRoomEncryptionSystem(roomId: string): EncryptionSystem {
kind: E2eeType.SHARED_KEY,
secret: storedPassword,
};
if (urlRoomId === roomId)
return {
kind: E2eeType.SHARED_KEY,
secret: passwordFromUrl,
};
if (room.hasEncryptionStateEvent()) {
return { kind: E2eeType.PER_PARTICIPANT };
}
return { kind: E2eeType.NONE };
}, [room, storedPassword]);
}, [passwordFromUrl, room, roomId, storedPassword, urlRoomId]);
return e2eeSystem;
}

View File

@@ -81,9 +81,8 @@ function sortRooms(client: MatrixClient, rooms: Room[]): Room[] {
}
const roomIsJoinable = (room: Room): boolean => {
const password = getKeyForRoom(room.roomId);
if (!room.hasEncryptionStateEvent() && !password) {
// if we have a non encrypted room (no encryption state event) we need a locally stored shared key.
if (!room.hasEncryptionStateEvent() && !getKeyForRoom(room.roomId)) {
// if we have an non encrypted room (no encryption state event) we need a locally stored shared key.
// in case this key also does not exists we cannot join the room.
return false;
}

View File

@@ -17,10 +17,9 @@ import {
type JSX,
} from "react";
import { createMediaDeviceObserver } from "@livekit/components-core";
import { combineLatest, distinctUntilChanged, map, startWith } from "rxjs";
import { combineLatest, map, startWith } from "rxjs";
import { useObservable, useObservableEagerState } from "observable-hooks";
import { logger } from "matrix-js-sdk/lib/logger";
import { deepCompare } from "matrix-js-sdk/lib/utils";
import {
useSetting,
@@ -141,16 +140,7 @@ function useMediaDeviceHandle(
kind,
() => logger.error("Error creating MediaDeviceObserver"),
requestPermissions,
).pipe(
startWith([]),
// This Observable emits new values whenever the browser fires a
// MediaDevices 'devicechange' event. One would think, innocently, that
// a 'devicechange' event means the devices have changed. But as of the
// time of writing, we are seeing mobile Safari firing spurious
// 'devicechange' events (where no change has actually occurred) when
// we call MediaDevices.getUserMedia. So, filter by deep equality.
distinctUntilChanged<MediaDeviceInfo[]>(deepCompare),
),
).pipe(startWith([])),
[kind, requestPermissions],
);
const available = useObservableEagerState(

View File

@@ -14,7 +14,7 @@ import {
type RoomOptions,
Track,
} from "livekit-client";
import { useEffect, useRef } from "react";
import { useEffect, useMemo, useRef } from "react";
import E2EEWorker from "livekit-client/e2ee-worker?worker";
import { logger } from "matrix-js-sdk/lib/logger";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
@@ -24,7 +24,11 @@ import { map } from "rxjs";
import { defaultLiveKitOptions } from "./options";
import { type SFUConfig } from "./openIDSFU";
import { type MuteStates } from "../room/MuteStates";
import { type MediaDeviceHandle, useMediaDevices } from "./MediaDevicesContext";
import {
type MediaDeviceHandle,
type MediaDevices,
useMediaDevices,
} from "./MediaDevicesContext";
import {
type ECConnectionState,
useECConnectionState,
@@ -36,9 +40,9 @@ import {
useTrackProcessor,
useTrackProcessorSync,
} from "./TrackProcessorContext";
import { useInitial } from "../useInitial";
import { observeTrackReference$ } from "../state/MediaViewModel";
import { useUrlParams } from "../UrlParams";
import { useInitial } from "../useInitial";
interface UseLivekitResult {
livekitRoom?: Room;
@@ -53,78 +57,27 @@ export function useLivekit(
): UseLivekitResult {
const { controlledAudioDevices } = useUrlParams();
const initialMuteStates = useInitial(() => muteStates);
const e2eeOptions = useMemo((): E2EEManagerOptions | undefined => {
if (e2eeSystem.kind === E2eeType.NONE) return undefined;
const devices = useMediaDevices();
const initialDevices = useInitial(() => devices);
// Store if audio/video are currently updating. If to prohibit unnecessary calls
// to setMicrophoneEnabled/setCameraEnabled
const audioMuteUpdating = useRef(false);
const videoMuteUpdating = useRef(false);
// Store the current button mute state that gets passed to this hook via props.
// We need to store it for awaited code that relies on the current value.
const buttonEnabled = useRef({
audio: initialMuteStates.audio.enabled,
video: initialMuteStates.video.enabled,
});
const { processor } = useTrackProcessor();
// Only ever create the room once via useInitial.
const room = useInitial(() => {
logger.info("[LivekitRoom] Create LiveKit room");
let e2ee: E2EEManagerOptions | undefined;
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
logger.info("Created MatrixKeyProvider (per participant)");
e2ee = {
return {
keyProvider: new MatrixKeyProvider(),
worker: new E2EEWorker(),
};
} else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) {
logger.info("Created ExternalE2EEKeyProvider (shared key)");
e2ee = {
return {
keyProvider: new ExternalE2EEKeyProvider(),
worker: new E2EEWorker(),
};
}
}, [e2eeSystem]);
const roomOptions: RoomOptions = {
...defaultLiveKitOptions,
videoCaptureDefaults: {
...defaultLiveKitOptions.videoCaptureDefaults,
deviceId: initialDevices.videoInput.selectedId,
processor,
},
audioCaptureDefaults: {
...defaultLiveKitOptions.audioCaptureDefaults,
deviceId: initialDevices.audioInput.selectedId,
},
audioOutput: {
deviceId: initialDevices.audioOutput.selectedId,
},
e2ee,
};
// We have to create the room manually here due to a bug inside
// @livekit/components-react. JSON.stringify() is used in deps of a
// useEffect() with an argument that references itself, if E2EE is enabled
const room = new Room(roomOptions);
room.setE2EEEnabled(e2eeSystem.kind !== E2eeType.NONE).catch((e) => {
logger.error("Failed to set E2EE enabled on room", e);
});
return room;
});
// Setup and update the keyProvider which was create by `createRoom`
useEffect(() => {
const e2eeOptions = room.options.e2ee;
if (
e2eeSystem.kind === E2eeType.NONE ||
!(e2eeOptions && "keyProvider" in e2eeOptions)
)
return;
if (e2eeSystem.kind === E2eeType.NONE || !e2eeOptions) return;
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
(e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession);
@@ -135,7 +88,56 @@ export function useLivekit(
logger.error("Failed to set shared key for E2EE", e);
});
}
}, [room.options.e2ee, e2eeSystem, rtcSession]);
}, [e2eeOptions, e2eeSystem, rtcSession]);
const initialMuteStates = useRef<MuteStates>(muteStates);
const devices = useMediaDevices();
const initialDevices = useRef<MediaDevices>(devices);
const { processor } = useTrackProcessor();
const initialProcessor = useInitial(() => processor);
const roomOptions = useMemo(
(): RoomOptions => ({
...defaultLiveKitOptions,
videoCaptureDefaults: {
...defaultLiveKitOptions.videoCaptureDefaults,
deviceId: initialDevices.current.videoInput.selectedId,
processor: initialProcessor,
},
audioCaptureDefaults: {
...defaultLiveKitOptions.audioCaptureDefaults,
deviceId: initialDevices.current.audioInput.selectedId,
},
audioOutput: {
deviceId: initialDevices.current.audioOutput.selectedId,
},
e2ee: e2eeOptions,
}),
[e2eeOptions, initialProcessor],
);
// Store if audio/video are currently updating. If to prohibit unnecessary calls
// to setMicrophoneEnabled/setCameraEnabled
const audioMuteUpdating = useRef(false);
const videoMuteUpdating = useRef(false);
// Store the current button mute state that gets passed to this hook via props.
// We need to store it for awaited code that relies on the current value.
const buttonEnabled = useRef({
audio: initialMuteStates.current.audio.enabled,
video: initialMuteStates.current.video.enabled,
});
// We have to create the room manually here due to a bug inside
// @livekit/components-react. JSON.stringify() is used in deps of a
// useEffect() with an argument that references itself, if E2EE is enabled
const room = useMemo(() => {
logger.info("[LivekitRooms] Create LiveKit room with options", roomOptions);
const r = new Room(roomOptions);
r.setE2EEEnabled(e2eeSystem.kind !== E2eeType.NONE).catch((e) => {
logger.error("Failed to set E2EE enabled on room", e);
});
return r;
}, [roomOptions, e2eeSystem]);
// Sync the requested track processors with LiveKit
useTrackProcessorSync(
@@ -157,8 +159,8 @@ export function useLivekit(
);
const connectionState = useECConnectionState(
initialDevices.audioInput.selectedId,
initialMuteStates.audio.enabled,
initialDevices.current.audioInput.selectedId,
initialMuteStates.current.audio.enabled,
room,
sfuConfig,
);

View File

@@ -5,15 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
SimpleSpanProcessor,
type SpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import opentelemetry, { type Tracer } from "@opentelemetry/api";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
import { logger } from "matrix-js-sdk/lib/logger";
import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor";
@@ -62,34 +59,34 @@ export class ElementCallOpenTelemetry {
collectorUrl: string | undefined,
rageshakeUrl: string | undefined,
) {
const spanProcessors: SpanProcessor[] = [];
// This is how we can make Jaeger show a reasonable service in the dropdown on the left.
const providerConfig = {
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
}),
};
this._provider = new WebTracerProvider(providerConfig);
if (collectorUrl) {
logger.info("Enabling OTLP collector with URL " + collectorUrl);
this.otlpExporter = new OTLPTraceExporter({
url: collectorUrl,
});
spanProcessors.push(new SimpleSpanProcessor(this.otlpExporter));
this._provider.addSpanProcessor(
new SimpleSpanProcessor(this.otlpExporter),
);
} else {
logger.info("OTLP collector disabled");
}
if (rageshakeUrl) {
this.rageshakeProcessor = new RageshakeSpanProcessor();
spanProcessors.push(this.rageshakeProcessor);
this._provider.addSpanProcessor(this.rageshakeProcessor);
}
spanProcessors.push(new PosthogSpanProcessor());
this._provider = new WebTracerProvider({
resource: resourceFromAttributes({
// This is how we can make Jaeger show a reasonable service in the dropdown on the left.
[ATTR_SERVICE_NAME]: SERVICE_NAME,
}),
spanProcessors,
});
this._provider.addSpanProcessor(new PosthogSpanProcessor());
opentelemetry.trace.setGlobalTracerProvider(this._provider);
this._tracer = opentelemetry.trace.getTracer(
// This is not the serviceName shown in jaeger
"my-element-call-otl-tracer",

View File

@@ -16,7 +16,7 @@ import {
} from "react";
import { Trans, useTranslation } from "react-i18next";
import {
ErrorSolidIcon,
ErrorIcon,
HostIcon,
OfflineIcon,
WebBrowserIcon,
@@ -63,7 +63,7 @@ const ErrorPage: FC<ErrorPageProps> = ({
icon = WebBrowserIcon;
break;
default:
icon = ErrorSolidIcon;
icon = ErrorIcon;
}
const actions: { label: string; onClick: () => void }[] = [];

View File

@@ -29,7 +29,6 @@ import { useAudioContext } from "../useAudioContext";
import { ActiveCall } from "./InCallView";
import {
flushPromises,
mockEmitter,
mockMatrixRoom,
mockMatrixRoomMember,
mockRtcMembership,
@@ -130,7 +129,6 @@ function createGroupCallView(
getMxcAvatarUrl: () => null,
getCanonicalAlias: () => null,
currentState: {
...mockEmitter(),
getJoinRule: () => JoinRule.Invite,
} as Partial<RoomState> as RoomState,
});

View File

@@ -43,10 +43,7 @@ import { MUTE_PARTICIPANT_COUNT, type MuteStates } from "./MuteStates";
import { useMediaDevices } from "../livekit/MediaDevicesContext";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
import {
saveKeyForRoom,
useRoomEncryptionSystem,
} from "../e2ee/sharedKeyManagement";
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName";
import { useJoinRule } from "./useJoinRule";
@@ -170,12 +167,6 @@ export const GroupCallView: FC<Props> = ({
useExperimentalToDeviceTransportSetting,
);
// Save the password once we start the groupCallView
const { password: passwordFromUrl } = useUrlParams();
useEffect(() => {
if (passwordFromUrl) saveKeyForRoom(room.roomId, passwordFromUrl);
}, [passwordFromUrl, room.roomId]);
usePageTitle(roomName);
const matrixInfo = useMemo((): MatrixInfo => {

View File

@@ -104,7 +104,6 @@ import { ConnectionLostError } from "../utils/errors.ts";
import { useTypedEventEmitter } from "../useEvents.ts";
import { MatrixAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx";
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships.ts";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
@@ -148,7 +147,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
logger.error("[Lifecycle] Failed to disconnect from livekit room", e);
});
};
}, [livekitRoom]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (livekitRoom !== undefined) {
@@ -250,7 +250,6 @@ export const InCallView: FC<InCallViewProps> = ({
useExperimentalToDeviceTransportSetting,
);
const encryptionSystem = useRoomEncryptionSystem(rtcSession.room.roomId);
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const showToDeviceEncryption = useMemo(
() =>
@@ -724,7 +723,10 @@ export const InCallView: FC<InCallViewProps> = ({
</Text>
)
}
<MatrixAudioRenderer members={memberships} muted={muteAllAudio} />
<MatrixAudioRenderer
members={rtcSession.memberships}
muted={muteAllAudio}
/>
{renderContent()}
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />

View File

@@ -53,7 +53,6 @@ import {
useTrackProcessorSync,
} from "../livekit/TrackProcessorContext";
import { usePageTitle } from "../usePageTitle";
import { useLatest } from "../useLatest";
interface Props {
client: MatrixClient;
@@ -160,14 +159,13 @@ export const LobbyView: FC<Props> = ({
],
);
const latestMuteStates = useLatest(muteStates);
const onError = useCallback(
(error: Error) => {
logger.error("Error while creating preview Tracks:", error);
latestMuteStates.current.audio.setEnabled?.(false);
latestMuteStates.current.video.setEnabled?.(false);
muteStates.audio.setEnabled?.(false);
muteStates.video.setEnabled?.(false);
},
[latestMuteStates],
[muteStates.audio, muteStates.video],
);
const tracks = usePreviewTracks(localTrackOptions, onError);

View File

@@ -60,7 +60,7 @@ function useMuteState(
// Determine the default value once devices are actually connected
(prev) =>
prev ?? (device.available.size > 0 ? enabledByDefault() : undefined),
[device.available.size],
[device],
);
return useMemo(
() =>
@@ -70,7 +70,7 @@ function useMuteState(
enabled: enabled ?? false,
setEnabled: setEnabled as Dispatch<SetStateAction<boolean>>,
},
[device.available.size, enabled, setEnabled],
[device, enabled, setEnabled],
);
}

View File

@@ -108,7 +108,7 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
class="error"
>
<div
class="_content_o77nw_8 icon"
class="_content_md016_17 icon"
data-size="large"
>
<svg
@@ -119,12 +119,12 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 3.5a1 1 0 1 0-1.414 1.414l4.113 4.114a5.5 5.5 0 0 0-.184 1.07A5.002 5.002 0 0 0 7 20h10.172l1.914 1.914A1 1 0 1 0 20.5 20.5l-.979-.979.004-.001L7.145 7.14l-.002.003zm18.5 12a4.48 4.48 0 0 1-.928 2.738L8.637 5.803A5.47 5.47 0 0 1 11.5 5a5.49 5.49 0 0 1 4.25 2.008 4 4 0 0 1 4.187 4.708A4.5 4.5 0 0 1 22 15.5"
d="M3.5 3.5a1 1 0 1 0-1.414 1.414l4.113 4.114a5.484 5.484 0 0 0-.184 1.069A5.002 5.002 0 0 0 7 20h10.172l1.914 1.914A1 1 0 0 0 20.5 20.5l-.979-.979.004-.002L7.145 7.14l-.002.004L3.5 3.5Zm18.5 12a4.48 4.48 0 0 1-.928 2.738L8.637 5.803A5.474 5.474 0 0 1 11.5 5a5.49 5.49 0 0 1 4.25 2.008 4 4 0 0 1 4.187 4.708A4.497 4.497 0 0 1 22 15.5Z"
/>
</svg>
</div>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
>
Connection lost
</h1>
@@ -135,7 +135,7 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
Reconnect
</button>
<button
class="_button_vczzf_8 homeLink"
class="_button_i91xf_17 homeLink"
data-kind="tertiary"
data-size="lg"
role="button"
@@ -258,7 +258,7 @@ exports[`should have a close button in widget mode 1`] = `
class="error"
>
<div
class="_content_o77nw_8 icon"
class="_content_md016_17 icon"
data-size="large"
>
<svg
@@ -269,17 +269,17 @@ exports[`should have a close button in widget mode 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.713 6.713A.97.97 0 0 1 16 7a.97.97 0 0 1-.713-.287A.97.97 0 0 1 15 6q0-.424.287-.713A.97.97 0 0 1 16 5q.424 0 .712.287Q17 5.576 17 6t-.288.713"
d="M16.712 6.713A.968.968 0 0 1 16 7a.968.968 0 0 1-.713-.287A.967.967 0 0 1 15 6c0-.283.096-.52.287-.713A.968.968 0 0 1 16 5a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713Z"
/>
<path
clip-rule="evenodd"
d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zm12 2H6v4h12zM6 12v-2h12v2zm0 2v2h12v-2zm0 6v-2h12v2z"
d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H6Zm12 2H6v4h12V4ZM6 12v-2h12v2H6Zm0 2v2h12v-2H6Zm0 6v-2h12v2H6Z"
fill-rule="evenodd"
/>
</svg>
</div>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
>
Call is not supported
</h1>
@@ -287,7 +287,7 @@ exports[`should have a close button in widget mode 1`] = `
The server is not configured to work with Element Call. Please contact your server admin (Domain: example.com, Error Code: MISSING_MATRIX_RTC_FOCUS).
</p>
<button
class="_button_vczzf_8"
class="_button_i91xf_17"
data-kind="primary"
data-size="lg"
role="button"
@@ -410,7 +410,7 @@ exports[`should render the error page with link back to home 1`] = `
class="error"
>
<div
class="_content_o77nw_8 icon"
class="_content_md016_17 icon"
data-size="large"
>
<svg
@@ -421,17 +421,17 @@ exports[`should render the error page with link back to home 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.713 6.713A.97.97 0 0 1 16 7a.97.97 0 0 1-.713-.287A.97.97 0 0 1 15 6q0-.424.287-.713A.97.97 0 0 1 16 5q.424 0 .712.287Q17 5.576 17 6t-.288.713"
d="M16.712 6.713A.968.968 0 0 1 16 7a.968.968 0 0 1-.713-.287A.967.967 0 0 1 15 6c0-.283.096-.52.287-.713A.968.968 0 0 1 16 5a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713Z"
/>
<path
clip-rule="evenodd"
d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zm12 2H6v4h12zM6 12v-2h12v2zm0 2v2h12v-2zm0 6v-2h12v2z"
d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H6Zm12 2H6v4h12V4ZM6 12v-2h12v2H6Zm0 2v2h12v-2H6Zm0 6v-2h12v2H6Z"
fill-rule="evenodd"
/>
</svg>
</div>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
>
Call is not supported
</h1>
@@ -439,7 +439,7 @@ exports[`should render the error page with link back to home 1`] = `
The server is not configured to work with Element Call. Please contact your server admin (Domain: example.com, Error Code: MISSING_MATRIX_RTC_FOCUS).
</p>
<button
class="_button_vczzf_8 homeLink"
class="_button_i91xf_17 homeLink"
data-kind="tertiary"
data-size="lg"
role="button"
@@ -562,7 +562,7 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
class="error"
>
<div
class="_content_o77nw_8 icon"
class="_content_md016_17 icon"
data-size="large"
>
<svg
@@ -573,17 +573,17 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.713 6.713A.97.97 0 0 1 16 7a.97.97 0 0 1-.713-.287A.97.97 0 0 1 15 6q0-.424.287-.713A.97.97 0 0 1 16 5q.424 0 .712.287Q17 5.576 17 6t-.288.713"
d="M16.712 6.713A.968.968 0 0 1 16 7a.968.968 0 0 1-.713-.287A.967.967 0 0 1 15 6c0-.283.096-.52.287-.713A.968.968 0 0 1 16 5a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713Z"
/>
<path
clip-rule="evenodd"
d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zm12 2H6v4h12zM6 12v-2h12v2zm0 2v2h12v-2zm0 6v-2h12v2z"
d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H6Zm12 2H6v4h12V4ZM6 12v-2h12v2H6Zm0 2v2h12v-2H6Zm0 6v-2h12v2H6Z"
fill-rule="evenodd"
/>
</svg>
</div>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
>
Call is not supported
</h1>
@@ -591,7 +591,7 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
The server is not configured to work with Element Call. Please contact your server admin (Domain: example.com, Error Code: MISSING_MATRIX_RTC_FOCUS).
</p>
<button
class="_button_vczzf_8 homeLink"
class="_button_i91xf_17 homeLink"
data-kind="tertiary"
data-size="lg"
role="button"
@@ -714,7 +714,7 @@ exports[`should report correct error for 'Connection lost' 1`] = `
class="error"
>
<div
class="_content_o77nw_8 icon"
class="_content_md016_17 icon"
data-size="large"
>
<svg
@@ -725,12 +725,12 @@ exports[`should report correct error for 'Connection lost' 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.5 3.5a1 1 0 1 0-1.414 1.414l4.113 4.114a5.5 5.5 0 0 0-.184 1.07A5.002 5.002 0 0 0 7 20h10.172l1.914 1.914A1 1 0 1 0 20.5 20.5l-.979-.979.004-.001L7.145 7.14l-.002.003zm18.5 12a4.48 4.48 0 0 1-.928 2.738L8.637 5.803A5.47 5.47 0 0 1 11.5 5a5.49 5.49 0 0 1 4.25 2.008 4 4 0 0 1 4.187 4.708A4.5 4.5 0 0 1 22 15.5"
d="M3.5 3.5a1 1 0 1 0-1.414 1.414l4.113 4.114a5.484 5.484 0 0 0-.184 1.069A5.002 5.002 0 0 0 7 20h10.172l1.914 1.914A1 1 0 0 0 20.5 20.5l-.979-.979.004-.002L7.145 7.14l-.002.004L3.5 3.5Zm18.5 12a4.48 4.48 0 0 1-.928 2.738L8.637 5.803A5.474 5.474 0 0 1 11.5 5a5.49 5.49 0 0 1 4.25 2.008 4 4 0 0 1 4.187 4.708A4.497 4.497 0 0 1 22 15.5Z"
/>
</svg>
</div>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
>
Connection lost
</h1>
@@ -741,7 +741,7 @@ exports[`should report correct error for 'Connection lost' 1`] = `
Reconnect
</button>
<button
class="_button_vczzf_8 homeLink"
class="_button_i91xf_17 homeLink"
data-kind="tertiary"
data-size="lg"
role="button"
@@ -864,7 +864,7 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
class="error"
>
<div
class="_content_o77nw_8 icon"
class="_content_md016_17 icon"
data-size="large"
>
<svg
@@ -875,12 +875,12 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 20q-.824 0-1.412-.587A1.93 1.93 0 0 1 2 18V6q0-.824.587-1.412A1.93 1.93 0 0 1 4 4h16q.824 0 1.413.588Q22 5.175 22 6v12q0 .824-.587 1.413A1.93 1.93 0 0 1 20 20zm0-2h16V8H4z"
d="M4 20c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 2 18V6c0-.55.196-1.02.587-1.412A1.926 1.926 0 0 1 4 4h16c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412v12c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 20 20H4Zm0-2h16V8H4v10Z"
/>
</svg>
</div>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
>
Incompatible browser
</h1>
@@ -888,7 +888,7 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
Your web browser does not support encrypted calls. Supported browsers include Chrome, Safari, and Firefox 117+.
</p>
<button
class="_button_vczzf_8 homeLink"
class="_button_i91xf_17 homeLink"
data-kind="tertiary"
data-size="lg"
role="button"
@@ -1011,7 +1011,7 @@ exports[`should report correct error for 'Insufficient capacity' 1`] = `
class="error"
>
<div
class="_content_o77nw_8 icon"
class="_content_md016_17 icon"
data-size="large"
>
<svg
@@ -1022,12 +1022,12 @@ exports[`should report correct error for 'Insufficient capacity' 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
>
Insufficient capacity
</h1>
@@ -1035,7 +1035,7 @@ exports[`should report correct error for 'Insufficient capacity' 1`] = `
The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists.
</p>
<button
class="_button_vczzf_8 homeLink"
class="_button_i91xf_17 homeLink"
data-kind="tertiary"
data-size="lg"
role="button"

View File

@@ -47,7 +47,7 @@ exports[`InCallView > rendering > renders 1`] = `
<button
aria-disabled="false"
aria-labelledby=":r0:"
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
data-testid="incall_mute"
@@ -63,14 +63,14 @@ exports[`InCallView > rendering > renders 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 8v-.006l6.831 6.832-.002.002 1.414 1.415.003-.003 1.414 1.414-.003.003L20.5 20.5a1 1 0 0 1-1.414 1.414l-3.022-3.022A7.95 7.95 0 0 1 13 19.938V21a1 1 0 0 1-2 0v-1.062A8 8 0 0 1 4 12a1 1 0 1 1 2 0 6 6 0 0 0 8.587 5.415l-1.55-1.55A4.005 4.005 0 0 1 8 12v-1.172L2.086 4.914A1 1 0 0 1 3.5 3.5zm9.417 6.583 1.478 1.477A7.96 7.96 0 0 0 20 12a1 1 0 0 0-2 0c0 .925-.21 1.8-.583 2.583M8.073 5.238l7.793 7.793q.132-.495.134-1.031V6a4 4 0 0 0-7.927-.762"
d="M8 8v-.006l6.831 6.832-.002.002 1.414 1.415.003-.003 1.414 1.414-.003.003L20.5 20.5a1 1 0 0 1-1.414 1.414l-3.022-3.022A7.949 7.949 0 0 1 13 19.938V21a1 1 0 0 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 1 1 2 0 6 6 0 0 0 8.587 5.415l-1.55-1.55A4.005 4.005 0 0 1 8 12v-1.172L2.086 4.914A1 1 0 0 1 3.5 3.5L8 8Zm9.417 6.583 1.478 1.477A7.963 7.963 0 0 0 20 12a1 1 0 0 0-2 0c0 .925-.21 1.8-.583 2.583ZM8.073 5.238l7.793 7.793c.087-.329.134-.674.134-1.031V6a4 4 0 0 0-7.927-.762Z"
/>
</svg>
</button>
<button
aria-disabled="false"
aria-labelledby=":r5:"
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
data-testid="incall_videomute"
@@ -86,13 +86,13 @@ exports[`InCallView > rendering > renders 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.747 2.753 4.35 4.355l.007-.003L18 17.994v.012l3.247 3.247a1 1 0 0 1-1.414 1.414l-2.898-2.898A2 2 0 0 1 16 20H6a4 4 0 0 1-4-4V8c0-.892.292-1.715.785-2.38L1.333 4.166a1 1 0 0 1 1.414-1.414M18 15.166 6.834 4H16a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715z"
d="M2.747 2.753 4.35 4.355l.007-.003L18 17.994v.012l3.247 3.247a1 1 0 0 1-1.414 1.414l-2.898-2.898A1.992 1.992 0 0 1 16 20H6a4 4 0 0 1-4-4V8c0-.892.292-1.715.785-2.38L1.333 4.166a1 1 0 0 1 1.414-1.414ZM18 15.166 6.834 4H16a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715v1.45Z"
/>
</svg>
</button>
<button
aria-labelledby=":ra:"
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary"
data-size="lg"
role="button"
@@ -107,13 +107,13 @@ exports[`InCallView > rendering > renders 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.731 2C13.432 2 14 2.568 14 3.269c0 .578.396 1.074.935 1.286q.128.052.253.106c.531.23 1.162.16 1.572-.25a1.27 1.27 0 0 1 1.794 0l1.034 1.035a1.27 1.27 0 0 1 0 1.794c-.41.41-.48 1.04-.248 1.572l.105.253c.212.539.708.935 1.286.935.701 0 1.269.568 1.269 1.269v1.462c0 .701-.568 1.269-1.269 1.269-.578 0-1.074.396-1.287.935q-.05.128-.104.253c-.232.531-.161 1.162.248 1.572a1.27 1.27 0 0 1 0 1.794l-1.034 1.034a1.27 1.27 0 0 1-1.794 0c-.41-.41-1.04-.48-1.572-.248a8 8 0 0 1-.253.105c-.539.212-.935.708-.935 1.286 0 .701-.568 1.269-1.269 1.269H11.27c-.702 0-1.27-.568-1.27-1.269 0-.578-.396-1.074-.935-1.287a8 8 0 0 1-.253-.104c-.531-.232-1.162-.161-1.572.248a1.27 1.27 0 0 1-1.794 0l-1.034-1.034a1.27 1.27 0 0 1 0-1.794c.41-.41.48-1.04.249-1.572a8 8 0 0 1-.106-.253C4.343 14.396 3.847 14 3.27 14 2.568 14 2 13.432 2 12.731V11.27c0-.702.568-1.27 1.269-1.27.578 0 1.074-.396 1.286-.935q.052-.128.106-.253c.23-.531.16-1.162-.25-1.572a1.27 1.27 0 0 1 0-1.794l1.035-1.034a1.27 1.27 0 0 1 1.794 0c.41.41 1.04.48 1.572.249a8 8 0 0 1 .253-.106c.539-.212.935-.708.935-1.286C10 2.568 10.568 2 11.269 2zM12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8"
d="M12.731 2C13.432 2 14 2.568 14 3.269c0 .578.396 1.074.935 1.286.085.034.17.07.253.106.531.23 1.162.16 1.572-.25a1.269 1.269 0 0 1 1.794 0l1.034 1.035a1.269 1.269 0 0 1 0 1.794c-.41.41-.48 1.04-.248 1.572.036.084.07.168.105.253.212.539.708.935 1.286.935.701 0 1.269.568 1.269 1.269v1.462c0 .701-.568 1.269-1.269 1.269-.578 0-1.074.396-1.287.935-.033.085-.068.17-.104.253-.232.531-.161 1.162.248 1.572a1.269 1.269 0 0 1 0 1.794l-1.034 1.034a1.269 1.269 0 0 1-1.794 0c-.41-.41-1.04-.48-1.572-.248a7.935 7.935 0 0 1-.253.105c-.539.212-.935.708-.935 1.286 0 .701-.568 1.269-1.269 1.269H11.27c-.702 0-1.27-.568-1.27-1.269 0-.578-.396-1.074-.935-1.287a7.975 7.975 0 0 1-.253-.104c-.531-.232-1.162-.161-1.572.248a1.269 1.269 0 0 1-1.794 0l-1.034-1.034a1.269 1.269 0 0 1 0-1.794c.41-.41.48-1.04.249-1.572a7.89 7.89 0 0 1-.106-.253C4.343 14.396 3.847 14 3.27 14 2.568 14 2 13.432 2 12.731V11.27c0-.702.568-1.27 1.269-1.27.578 0 1.074-.396 1.286-.935.034-.085.07-.17.106-.253.23-.531.16-1.162-.25-1.572a1.269 1.269 0 0 1 0-1.794l1.035-1.034a1.269 1.269 0 0 1 1.794 0c.41.41 1.04.48 1.572.249a7.93 7.93 0 0 1 .253-.106c.539-.212.935-.708.935-1.286C10 2.568 10.568 2 11.269 2h1.462ZM12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"
/>
</svg>
</button>
<button
aria-labelledby=":rf:"
class="_button_vczzf_8 endCall _has-icon_vczzf_57 _icon-only_vczzf_50 _destructive_vczzf_107"
class="_button_i91xf_17 endCall _has-icon_i91xf_66 _icon-only_i91xf_59 _destructive_i91xf_116"
data-kind="primary"
data-size="lg"
data-testid="incall_leave"
@@ -129,7 +129,7 @@ exports[`InCallView > rendering > renders 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m2.765 16.02-2.47-2.416A1.02 1.02 0 0 1 0 12.852q0-.456.295-.751a15.6 15.6 0 0 1 5.316-3.786A15.9 15.9 0 0 1 12 7q3.355 0 6.39 1.329a16 16 0 0 1 5.315 3.772q.295.294.295.751t-.295.752l-2.47 2.416a1.047 1.047 0 0 1-1.396.108l-3.114-2.363a1.1 1.1 0 0 1-.322-.376 1.1 1.1 0 0 1-.108-.483v-2.27a13.6 13.6 0 0 0-2.12-.524C13.459 9.996 12 9.937 12 9.937s-1.459.059-2.174.175q-1.074.174-2.121.523v2.271q0 .268-.108.483a1.1 1.1 0 0 1-.322.376l-3.114 2.363a1.047 1.047 0 0 1-1.396-.107"
d="m2.765 16.02-2.47-2.416A1.018 1.018 0 0 1 0 12.852c0-.304.098-.555.295-.751a15.64 15.64 0 0 1 5.316-3.786A15.89 15.89 0 0 1 12 7c2.237 0 4.367.443 6.39 1.329a15.977 15.977 0 0 1 5.315 3.772c.197.196.295.447.295.751 0 .305-.098.555-.295.752l-2.47 2.416a1.047 1.047 0 0 1-1.396.108l-3.114-2.363a1.067 1.067 0 0 1-.322-.376 1.066 1.066 0 0 1-.108-.483v-2.27a13.593 13.593 0 0 0-2.12-.524C13.459 9.996 12 9.937 12 9.937s-1.459.059-2.175.175c-.715.116-1.422.29-2.12.523v2.271c0 .179-.036.34-.108.483a1.066 1.066 0 0 1-.322.376l-3.114 2.363a1.047 1.047 0 0 1-1.396-.107Z"
/>
</svg>
</button>
@@ -152,7 +152,7 @@ exports[`InCallView > rendering > renders 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 5h14v8h-5a1 1 0 0 0-1 1v5H5zm10 14v-4h4v4zM5 21h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2"
d="M5 5h14v8h-5a1 1 0 0 0-1 1v5H5V5Zm10 14v-4h4v4h-4ZM5 21h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2Z"
/>
</svg>
<input
@@ -171,7 +171,7 @@ exports[`InCallView > rendering > renders 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 11a.97.97 0 0 1-.712-.287A.97.97 0 0 1 3 10V4q0-.424.288-.712A.97.97 0 0 1 4 3h6q.424 0 .713.288Q11 3.575 11 4v6q0 .424-.287.713A.97.97 0 0 1 10 11zm5-2V5H5v4zm5 12a.97.97 0 0 1-.713-.288A.97.97 0 0 1 13 20v-6q0-.424.287-.713A.97.97 0 0 1 14 13h6q.424 0 .712.287.288.288.288.713v6q0 .424-.288.712A.97.97 0 0 1 20 21zm5-2v-4h-4v4zM4 21a.97.97 0 0 1-.712-.288A.97.97 0 0 1 3 20v-6q0-.424.288-.713A.97.97 0 0 1 4 13h6q.424 0 .713.287.287.288.287.713v6q0 .424-.287.712A.97.97 0 0 1 10 21zm5-2v-4H5v4zm5-8a.97.97 0 0 1-.713-.287A.97.97 0 0 1 13 10V4q0-.424.287-.712A.97.97 0 0 1 14 3h6q.424 0 .712.288Q21 3.575 21 4v6q0 .424-.288.713A.97.97 0 0 1 20 11zm5-2V5h-4v4z"
d="M4 11a.967.967 0 0 1-.712-.287A.968.968 0 0 1 3 10V4c0-.283.096-.52.288-.712A.968.968 0 0 1 4 3h6a.97.97 0 0 1 .713.288A.968.968 0 0 1 11 4v6c0 .283-.096.52-.287.713A.968.968 0 0 1 10 11H4Zm5-2V5H5v4h4Zm5 12a.968.968 0 0 1-.713-.288A.968.968 0 0 1 13 20v-6c0-.283.096-.52.287-.713A.968.968 0 0 1 14 13h6a.97.97 0 0 1 .712.287c.192.192.288.43.288.713v6c0 .283-.096.52-.288.712A.968.968 0 0 1 20 21h-6Zm5-2v-4h-4v4h4ZM4 21a.967.967 0 0 1-.712-.288A.968.968 0 0 1 3 20v-6a.97.97 0 0 1 .288-.713A.967.967 0 0 1 4 13h6c.283 0 .52.096.713.287.191.192.287.43.287.713v6a.97.97 0 0 1-.287.712A.968.968 0 0 1 10 21H4Zm5-2v-4H5v4h4Zm5-8a.968.968 0 0 1-.713-.287A.968.968 0 0 1 13 10V4a.97.97 0 0 1 .287-.712A.968.968 0 0 1 14 3h6c.283 0 .52.096.712.288A.965.965 0 0 1 21 4v6a.97.97 0 0 1-.288.713A.968.968 0 0 1 20 11h-6Zm5-2V5h-4v4h4Z"
/>
</svg>
</div>

View File

@@ -9,13 +9,11 @@ import {
type MatrixRTCSession,
MatrixRTCSessionEvent,
} from "matrix-js-sdk/lib/matrixrtc";
import { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useState } from "react";
import { deepCompare } from "matrix-js-sdk/lib/utils";
import { logger } from "matrix-js-sdk/lib/logger";
import { type LivekitFocus, isLivekitFocus } from "matrix-js-sdk/lib/matrixrtc";
import { useTypedEventEmitterState } from "../useEvents";
/**
* Gets the currently active (livekit) focus for a MatrixRTC session
* This logic is specific to livekit foci where the whole call must use one
@@ -24,27 +22,38 @@ import { useTypedEventEmitterState } from "../useEvents";
export function useActiveLivekitFocus(
rtcSession: MatrixRTCSession,
): LivekitFocus | undefined {
const activeFocus = useTypedEventEmitterState(
rtcSession,
MatrixRTCSessionEvent.MembershipsChanged,
useCallback(() => {
const f = rtcSession.getActiveFocus();
// Only handle foci with type="livekit" for now.
return !!f && isLivekitFocus(f) ? f : undefined;
}, [rtcSession]),
);
const [activeFocus, setActiveFocus] = useState(() => {
const f = rtcSession.getActiveFocus();
// Only handle foci with type="livekit" for now.
return !!f && isLivekitFocus(f) ? f : undefined;
});
const prevActiveFocus = useRef(activeFocus);
useEffect(() => {
if (!deepCompare(activeFocus, prevActiveFocus.current)) {
const onMembershipsChanged = useCallback(() => {
const newActiveFocus = rtcSession.getActiveFocus();
if (!!newActiveFocus && !isLivekitFocus(newActiveFocus)) return;
if (!deepCompare(activeFocus, newActiveFocus)) {
const oldestMembership = rtcSession.getOldestMembership();
logger.warn(
`Got new active focus from membership: ${oldestMembership?.sender}/${oldestMembership?.deviceId}.
Updated focus (focus switch) from ${JSON.stringify(prevActiveFocus.current)} to ${JSON.stringify(activeFocus)}`,
Updating focus (focus switch) from ${JSON.stringify(activeFocus)} to ${JSON.stringify(newActiveFocus)}`,
);
prevActiveFocus.current = activeFocus;
setActiveFocus(newActiveFocus);
}
}, [activeFocus, rtcSession]);
useEffect(() => {
rtcSession.on(
MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged,
);
return (): void => {
rtcSession.off(
MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged,
);
};
});
return activeFocus;
}

View File

@@ -1,19 +1,18 @@
/*
Copyright 2025 New Vector Ltd.
Copyright 2023, 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type Room, RoomEvent } from "matrix-js-sdk";
import { useCallback } from "react";
import { useState } from "react";
import { useTypedEventEmitterState } from "../useEvents";
import { useTypedEventEmitter } from "../useEvents";
export function useRoomName(room: Room): string {
return useTypedEventEmitterState(
room,
RoomEvent.Name,
useCallback(() => room.name, [room]),
);
const [, setNumUpdates] = useState(0);
// Whenever the name changes, force an update
useTypedEventEmitter(room, RoomEvent.Name, () => setNumUpdates((n) => n + 1));
return room.name;
}

View File

@@ -5,15 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { useCallback } from "react";
import {
type RoomState,
RoomStateEvent,
type Room,
RoomEvent,
} from "matrix-js-sdk";
import { useCallback, useMemo, useState } from "react";
import { type RoomState, RoomStateEvent, type Room } from "matrix-js-sdk";
import { useTypedEventEmitterState } from "../useEvents";
import { useTypedEventEmitter } from "../useEvents";
/**
* A React hook for values computed from room state.
@@ -21,17 +16,14 @@ import { useTypedEventEmitterState } from "../useEvents";
* @param f A mapping from the current room state to the computed value.
* @returns The computed value.
*/
export function useRoomState<T>(room: Room, f: (state: RoomState) => T): T {
// TODO: matrix-js-sdk says that Room.currentState is deprecated, but it's not
// clear how to reactively track the current state of the room without it
const currentState = useTypedEventEmitterState(
export const useRoomState = <T>(room: Room, f: (state: RoomState) => T): T => {
const [numUpdates, setNumUpdates] = useState(0);
useTypedEventEmitter(
room,
RoomEvent.CurrentStateUpdated,
useCallback(() => room.currentState, [room]),
);
return useTypedEventEmitterState(
currentState,
RoomStateEvent.Update,
useCallback(() => f(currentState), [f, currentState]),
useCallback(() => setNumUpdates((n) => n + 1), [setNumUpdates]),
);
}
// We want any change to the update counter to trigger an update here
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => f(room.currentState), [room, f, numUpdates]);
};

View File

@@ -57,7 +57,7 @@ export const FeedbackSettingsTab: FC<Props> = ({ roomId }) => {
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
const optInDescription = (
<Text size="sm" as="span">
<Text size="sm">
<Trans i18nKey="settings.opt_in_description">
<AnalyticsNotice />
<br />

View File

@@ -129,7 +129,7 @@ export const SettingsModal: FC<Props> = ({
}
/>
)}
{iosDeviceMenu && controlledAudioDevices && (
{iosDeviceMenu && (
<Button
onClick={(e): void => {
e.preventDefault();

View File

@@ -26,11 +26,11 @@ import {
type RemoteParticipant,
} from "livekit-client";
import * as ComponentsCore from "@livekit/components-core";
import { isEqual } from "lodash-es";
import {
type CallMembership,
type MatrixRTCSession,
} from "matrix-js-sdk/lib/matrixrtc";
import { deepCompare } from "matrix-js-sdk/lib/utils";
import { CallViewModel, type Layout } from "./CallViewModel";
import {
@@ -200,7 +200,7 @@ function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
// care about the most recent value for each time step, so discard these
// extra values.
debounceTime(0),
distinctUntilChanged(deepCompare),
distinctUntilChanged(isEqual),
);
}

View File

@@ -36,7 +36,6 @@ export function TabContainer<K extends Key>({
<NavBar role="tablist" aria-label={label} className={styles.tabList}>
{tabs.map(({ key, name }) => (
<NavItem
key={key}
aria-controls={`${idPrefix}[${key}]`}
onClick={() => onTabChange(key)}
active={key === tab}
@@ -47,7 +46,6 @@ export function TabContainer<K extends Key>({
</NavBar>
{tabs.map(({ key, content }) => (
<div
key={key}
id={`${idPrefix}[${key}]`}
style={{ display: key === tab ? undefined : "none" }}
>

View File

@@ -13,7 +13,7 @@ import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { VideoTrack } from "@livekit/components-react";
import { Text, Tooltip } from "@vector-im/compound-web";
import { ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import styles from "./MediaView.module.css";
import { Avatar } from "../Avatar";
@@ -168,7 +168,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
placement="bottom"
isTriggerInteractive={false}
>
<ErrorSolidIcon
<ErrorIcon
width={20}
height={20}
className={styles.errorIcon}

View File

@@ -1,88 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { test } from "vitest";
import { render, screen } from "@testing-library/react";
import { type FC, useEffect, useState } from "react";
import userEvent from "@testing-library/user-event";
import { TypedEventEmitter } from "matrix-js-sdk";
import { useTypedEventEmitterState } from "./useEvents";
class TestEmitter extends TypedEventEmitter<"change", { change: () => void }> {
private state = 1;
public readonly getState = (): number => this.state;
public readonly getNegativeState = (): number => -this.state;
public readonly setState = (value: number): void => {
this.state = value;
this.emit("change");
};
}
test("useTypedEventEmitterState reacts to events", async () => {
const user = userEvent.setup();
const emitter = new TestEmitter();
const Test: FC = () => {
const value = useTypedEventEmitterState(
emitter,
"change",
emitter.getState,
);
return (
<>
<button onClick={() => emitter.setState(2)}>Change value</button>
<div>Value is {value}</div>
</>
);
};
render(<Test />);
screen.getByText("Value is 1");
await user.click(screen.getByText("Change value"));
screen.getByText("Value is 2");
});
test("useTypedEventEmitterState reacts to changes made by an effect mounted on the same render", () => {
const emitter = new TestEmitter();
const Test: FC = () => {
useEffect(() => emitter.setState(2), []);
const value = useTypedEventEmitterState(
emitter,
"change",
emitter.getState,
);
return `Value is ${value}`;
};
render(<Test />);
screen.getByText("Value is 2");
});
test("useTypedEventEmitterState reacts to changes in getState", async () => {
const user = userEvent.setup();
const emitter = new TestEmitter();
const Test: FC = () => {
const [fn, setFn] = useState(() => emitter.getState);
const value = useTypedEventEmitterState(emitter, "change", fn);
return (
<>
<button onClick={() => setFn(() => emitter.getNegativeState)}>
Change getState
</button>
<div>Value is {value}</div>
</>
);
};
render(<Test />);
screen.getByText("Value is 1");
await user.click(screen.getByText("Change getState"));
screen.getByText("Value is -1");
});

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { useCallback, useEffect, useSyncExternalStore } from "react";
import { useEffect } from "react";
import type {
Listener,
@@ -13,9 +13,7 @@ import type {
TypedEventEmitter,
} from "matrix-js-sdk/lib/models/typed-event-emitter";
/**
* Shortcut for registering a listener on an EventTarget.
*/
// Shortcut for registering a listener on an EventTarget
export function useEventTarget<T extends Event>(
target: EventTarget | null | undefined,
eventType: string,
@@ -35,9 +33,7 @@ export function useEventTarget<T extends Event>(
}, [target, eventType, listener, options]);
}
/**
* Shortcut for registering a listener on a TypedEventEmitter.
*/
// Shortcut for registering a listener on a TypedEventEmitter
export function useTypedEventEmitter<
Events extends string,
Arguments extends ListenerMap<Events>,
@@ -54,33 +50,3 @@ export function useTypedEventEmitter<
};
}, [emitter, eventType, listener]);
}
/**
* Reactively tracks a value which is recalculated whenever the provided event
* emitter emits an event. This is useful for bridging state from matrix-js-sdk
* into React.
*/
export function useTypedEventEmitterState<
Events extends string,
Arguments extends ListenerMap<Events>,
T extends Events,
State,
>(
emitter: TypedEventEmitter<Events, Arguments>,
eventType: T,
getState: () => State,
): State {
const subscribe = useCallback(
(onChange: () => void) => {
emitter.on(eventType, onChange as Listener<Events, Arguments, T>);
return (): void => {
emitter.off(eventType, onChange as Listener<Events, Arguments, T>);
};
},
[emitter, eventType],
);
// See the React docs for useSyncExternalStore; given that we're trying to
// bridge state from an external source into React, using this hook is exactly
// what React recommends.
return useSyncExternalStore(subscribe, getState);
}

View File

@@ -1,51 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { test } from "vitest";
import { render, screen } from "@testing-library/react";
import { type FC, useEffect, useState } from "react";
import userEvent from "@testing-library/user-event";
import {
setLocalStorageItemReactive,
useLocalStorage,
} from "./useLocalStorage";
test("useLocalStorage reacts to changes made by an effect mounted on the same render", () => {
localStorage.clear();
const Test: FC = () => {
useEffect(() => setLocalStorageItemReactive("my-value", "Hello!"), []);
const [myValue] = useLocalStorage("my-value");
return myValue;
};
render(<Test />);
screen.getByText("Hello!");
});
test("useLocalStorage reacts to key changes", async () => {
localStorage.clear();
localStorage.setItem("value-1", "1");
localStorage.setItem("value-2", "2");
const Test: FC = () => {
const [key, setKey] = useState("value-1");
const [value] = useLocalStorage(key);
if (key !== `value-${value}`) throw new Error("Value is out of sync");
return (
<>
<button onClick={() => setKey("value-2")}>Switch keys</button>
<div>Value is: {value}</div>
</>
);
};
const user = userEvent.setup();
render(<Test />);
screen.getByText("Value is: 1");
await user.click(screen.getByRole("button", { name: "Switch keys" }));
screen.getByText("Value is: 2");
});

View File

@@ -5,44 +5,51 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { useCallback } from "react";
import { TypedEventEmitter } from "matrix-js-sdk";
import { useTypedEventEmitterState } from "./useEvents";
import EventEmitter from "events";
import { useCallback, useEffect, useState } from "react";
type LocalStorageItem = ReturnType<typeof localStorage.getItem>;
// Bus to notify other useLocalStorage consumers when an item is changed
export const localStorageBus = new TypedEventEmitter<
string,
{ [key: string]: () => void }
>();
export const localStorageBus = new EventEmitter();
/**
* Like useState, but reads from and persists the value to localStorage
* This hook will not update when we write to localStorage.setItem(key, value) directly.
* For the hook to react either use the returned setter or `setLocalStorageItemReactive`.
*/
export function useLocalStorage(
// Like useState, but reads from and persists the value to localStorage
export const useLocalStorage = (
key: string,
): [LocalStorageItem, (value: string) => void] {
const value = useTypedEventEmitterState(
localStorageBus,
key,
useCallback(() => localStorage.getItem(key), [key]),
);
const setValue = useCallback(
(newValue: string) => setLocalStorageItemReactive(key, newValue),
[key],
): [LocalStorageItem, (value: string) => void] => {
const [value, setValue] = useState<LocalStorageItem>(() =>
localStorage.getItem(key),
);
return [value, setValue];
}
useEffect(() => {
localStorageBus.on(key, setValue);
return (): void => {
localStorageBus.off(key, setValue);
};
}, [key, setValue]);
export const setLocalStorageItemReactive = (
key: string,
value: string,
): void => {
localStorage.setItem(key, value);
localStorageBus.emit(key);
return [
value,
useCallback(
(newValue: string) => {
setValue(newValue);
localStorage.setItem(key, newValue);
localStorageBus.emit(key, newValue);
},
[key, setValue],
),
];
};
export const setLocalStorageItem = (key: string, value: string): void => {
// Avoid unnecessary updates. Not avoiding them so can cause unexpected state updates across hooks.
// For instance:
// - In call view uses useRoomEncryptionSystem
// - This will set the key again.
// - All other instances of useRoomEncryptionSystem will now do a useMemo update of the e2eeSystem
// - because the dependency `storedPassword = useInternalRoomSharedKey(roomId);` would change.
if (localStorage.getItem(key) === value) return;
localStorage.setItem(key, value);
localStorageBus.emit(key, value);
};

View File

@@ -10,31 +10,33 @@ import {
type MatrixRTCSession,
MatrixRTCSessionEvent,
} from "matrix-js-sdk/lib/matrixrtc";
import { TypedEventEmitter } from "matrix-js-sdk";
import { useCallback, useEffect } from "react";
import { useTypedEventEmitterState } from "./useEvents";
const dummySession = new TypedEventEmitter();
import { useEffect, useState } from "react";
export function useMatrixRTCSessionJoinState(
rtcSession: MatrixRTCSession | undefined,
): boolean {
// React doesn't allow you to run a hook conditionally, so we have to plug in
// a dummy event emitter in case there is no rtcSession yet
const isJoined = useTypedEventEmitterState(
rtcSession ?? dummySession,
MatrixRTCSessionEvent.JoinStateChanged,
useCallback(() => rtcSession?.isJoined() ?? false, [rtcSession]),
);
const [, setNumUpdates] = useState(0);
useEffect(() => {
logger.info(
`Session in room ${rtcSession?.room.roomId} changed to ${
isJoined ? "joined" : "left"
}`,
);
}, [rtcSession, isJoined]);
if (rtcSession !== undefined) {
const onJoinStateChanged = (isJoined: boolean): void => {
logger.info(
`Session in room ${rtcSession.room.roomId} changed to ${
isJoined ? "joined" : "left"
}`,
);
setNumUpdates((n) => n + 1); // Force an update
};
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged);
return isJoined;
return (): void => {
rtcSession.off(
MatrixRTCSessionEvent.JoinStateChanged,
onJoinStateChanged,
);
};
}
}, [rtcSession]);
return rtcSession?.isJoined() ?? false;
}

View File

@@ -5,21 +5,39 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { logger } from "matrix-js-sdk/lib/logger";
import {
type CallMembership,
type MatrixRTCSession,
MatrixRTCSessionEvent,
} from "matrix-js-sdk/lib/matrixrtc";
import { useCallback } from "react";
import { useTypedEventEmitterState } from "./useEvents";
import { useCallback, useEffect, useState } from "react";
export function useMatrixRTCSessionMemberships(
rtcSession: MatrixRTCSession,
): CallMembership[] {
return useTypedEventEmitterState(
rtcSession,
MatrixRTCSessionEvent.MembershipsChanged,
useCallback(() => rtcSession.memberships, [rtcSession]),
);
const [memberships, setMemberships] = useState(rtcSession.memberships);
const onMembershipsChanged = useCallback(() => {
logger.info(
`Memberships changed for call in room ${rtcSession.room.roomId} (${rtcSession.memberships.length} members)`,
);
setMemberships(rtcSession.memberships);
}, [rtcSession]);
useEffect(() => {
rtcSession.on(
MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged,
);
return (): void => {
rtcSession.off(
MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged,
);
};
}, [rtcSession, onMembershipsChanged]);
return memberships;
}

View File

@@ -1,11 +1,13 @@
/*
Copyright 2023-2025 New Vector Ltd.
Copyright 2023, 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { useCallback, useMemo, useSyncExternalStore } from "react";
import { useCallback, useMemo, useState } from "react";
import { useEventTarget } from "./useEvents";
/**
* React hook that tracks whether the given media query matches.
@@ -13,13 +15,14 @@ import { useCallback, useMemo, useSyncExternalStore } from "react";
export function useMediaQuery(query: string): boolean {
const mediaQuery = useMemo(() => window.matchMedia(query), [query]);
const subscribe = useCallback(
(onChange: () => void) => {
mediaQuery.addEventListener("change", onChange);
return (): void => mediaQuery.removeEventListener("change", onChange);
},
[mediaQuery],
const [numChanges, setNumChanges] = useState(0);
useEventTarget(
mediaQuery,
"change",
useCallback(() => setNumChanges((n) => n + 1), [setNumChanges]),
);
const getState = useCallback(() => mediaQuery.matches, [mediaQuery]);
return useSyncExternalStore(subscribe, getState);
// We want any change to the update counter to trigger an update here
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => mediaQuery.matches, [mediaQuery, numChanges]);
}

View File

@@ -108,7 +108,7 @@ interface EmitterMock<T> {
removeListener: () => T;
}
export function mockEmitter<T>(): EmitterMock<T> {
function mockEmitter<T>(): EmitterMock<T> {
return {
on(): T {
return this as T;

View File

@@ -22,9 +22,6 @@
// These imports within @livekit/components-core and
// @livekit/components-react are broken under the "bundler" module
// resolution mode, so we need to resolve them manually
"livekit-client/dist/src/room/types": [
"./node_modules/livekit-client/dist/src/room/types.d.ts"
],
"livekit-client/dist/src/room/Room": [
"./node_modules/livekit-client/dist/src/room/Room.d.ts"
],

View File

@@ -89,7 +89,6 @@ export default defineConfig(({ mode, packageType }) => {
},
},
build: {
minify: mode === "production" ? true : false,
sourcemap: true,
rollupOptions: {
output: {

3988
yarn.lock

File diff suppressed because it is too large Load Diff