mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-23 20:59:21 +00:00
Compare commits
42 Commits
robin/deep
...
robin/erro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a36e72147c | ||
|
|
905177f0b6 | ||
|
|
31c35583fb | ||
|
|
764c6fce24 | ||
|
|
86aa459a84 | ||
|
|
32b6250cc3 | ||
|
|
4a6dbf350f | ||
|
|
29968a3eba | ||
|
|
aa092c2195 | ||
|
|
0bf5c4a835 | ||
|
|
3d47609cff | ||
|
|
1425de8726 | ||
|
|
c1aa1888f0 | ||
|
|
58ebbfeed7 | ||
|
|
02f3f1b71a | ||
|
|
f7a6e20ae4 | ||
|
|
378d59e948 | ||
|
|
a07dbe645e | ||
|
|
a596eb71a9 | ||
|
|
c8a1cae18b | ||
|
|
4f1450aaf0 | ||
|
|
8704b44b78 | ||
|
|
0ee11af3c5 | ||
|
|
13fac57b01 | ||
|
|
e0ce58b2a2 | ||
|
|
6f9adfe3e8 | ||
|
|
d47e579994 | ||
|
|
61d8f9a412 | ||
|
|
31185ae365 | ||
|
|
e07c7c864b | ||
|
|
b88bce5220 | ||
|
|
775e110ca3 | ||
|
|
97d548ee3a | ||
|
|
34724b7a8c | ||
|
|
b005f36ac7 | ||
|
|
b0587fcfb3 | ||
|
|
b8eccc6d7a | ||
|
|
34df584366 | ||
|
|
a62b0be831 | ||
|
|
01240fb59d | ||
|
|
7851b9c289 | ||
|
|
789f37611b |
21
.github/workflows/build-element-call.yaml
vendored
21
.github/workflows/build-element-call.yaml
vendored
@@ -9,6 +9,11 @@ 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
|
||||
@@ -37,20 +42,8 @@ jobs:
|
||||
node-version-file: ".node-version"
|
||||
- name: Install dependencies
|
||||
run: "yarn install --immutable"
|
||||
- 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"
|
||||
- name: Build Element Call
|
||||
run: ${{ format('yarn run build:{0}:{1}', inputs.package, inputs.build_mode) }}
|
||||
env:
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@@ -14,6 +14,7 @@ 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 }}
|
||||
@@ -61,6 +62,7 @@ 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 }}
|
||||
|
||||
6
.github/workflows/test.yaml
vendored
6
.github/workflows/test.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
playwright:
|
||||
name: Run end-to-end tests
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -49,9 +49,9 @@ jobs:
|
||||
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 ps
|
||||
- name: Copy config file
|
||||
run: cp config/config.devenv.json public/config.json
|
||||
- name: Run Playwright tests
|
||||
env:
|
||||
USE_DOCKER: 1
|
||||
run: yarn playwright test
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
@@ -6,7 +6,7 @@ COPY ./dist /dist
|
||||
WORKDIR /dist/assets
|
||||
RUN gzip -k ../index.html *.js *.map *.css *.wasm *-app-*.json
|
||||
|
||||
FROM nginxinc/nginx-unprivileged:alpine
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim
|
||||
|
||||
COPY --from=builder ./dist /app
|
||||
|
||||
|
||||
@@ -8,5 +8,12 @@
|
||||
"features": {
|
||||
"feature_use_device_session_member_events": true
|
||||
},
|
||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf"
|
||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||
"matrix_rtc_session": {
|
||||
"wait_for_key_rotation_ms": 3000,
|
||||
"membership_event_expiry_ms": 180000000,
|
||||
"delayed_leave_event_delay_ms": 18000,
|
||||
"delayed_leave_event_restart_ms": 4000,
|
||||
"network_error_retry_ms": 4000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,12 @@
|
||||
"features": {
|
||||
"feature_use_device_session_member_events": true
|
||||
},
|
||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf"
|
||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||
"matrix_rtc_session": {
|
||||
"wait_for_key_rotation_ms": 3000,
|
||||
"membership_event_expiry_ms": 180000000,
|
||||
"delayed_leave_event_delay_ms": 18000,
|
||||
"delayed_leave_event_restart_ms": 4000,
|
||||
"network_error_retry_ms": 4000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
"server_name": "call-unstable.ems.host"
|
||||
}
|
||||
},
|
||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||
"matrix_rtc_session": {
|
||||
"wait_for_key_rotation_ms": 3000,
|
||||
"membership_event_expiry_ms": 180000000,
|
||||
"delayed_leave_event_delay_ms": 18000,
|
||||
"delayed_leave_event_restart_ms": 4000,
|
||||
"network_error_retry_ms": 4000
|
||||
},
|
||||
"posthog": {
|
||||
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",
|
||||
"api_host": "https://posthog-element-call.element.io"
|
||||
|
||||
@@ -4,6 +4,7 @@ networks:
|
||||
services:
|
||||
auth-service:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
|
||||
pull_policy: always
|
||||
hostname: auth-server
|
||||
environment:
|
||||
- LIVEKIT_JWT_PORT=6080
|
||||
|
||||
@@ -19,3 +19,7 @@ On mobile platforms (iOS, Android), web views do not reliably support selecting
|
||||
- `controls.setAudioEnabled(enabled: boolean)` Enables/disables all audio output from the application. Output is enabled by default.
|
||||
- `showNativeAudioDevicePicker: (() => void) | undefined`. Callback called whenever the user presses the output button in the settings menu.
|
||||
This button is only shown on iOS. (`userAgent.includes("iPhone")`)
|
||||
- `controls.onAudioPlaybackStarted: ((id: string) => void) | undefined`: This will be called the first time we start
|
||||
playing audio in the webview. It can be helpful to do device setup on the native app when the webviews audio is ready.
|
||||
In particular android is using it to setup the output channel so that the call volume can
|
||||
be controlled by the hardware volume rocker.
|
||||
|
||||
@@ -255,13 +255,17 @@ self-hosters and developers working with Element Call.
|
||||
|
||||
- [How to resolve stuck MatrixRTC calls](https://sspaeth.de/2025/02/how-to-resolve-stuck-matrixrtc-calls/)
|
||||
|
||||
## 🛠️ How-Tos & Tutorials
|
||||
## 📝 How-Tos & Tutorials
|
||||
|
||||
- [MatrixRTC aka Element-call setup (Geek warning)](https://sspaeth.de/2024/11/sfu/)
|
||||
- [MatrixRTC with Synology Container Manager (Docker)](https://ztfr.de/matrixrtc-with-synology-container-manager-docker/)
|
||||
- [Encrypted & Scalable Video Calls: How to deploy an Element Call backend with Synapse Using Docker-Compose](https://willlewis.co.uk/blog/posts/deploy-element-call-backend-with-synapse-and-docker-compose/)
|
||||
- [Element Call einrichten: Verschlüsselte Videoanrufe mit Element X und Matrix Synapse](https://www.cleveradmin.de/blog/2025/04/matrixrtc-element-call-backend-einrichten/)
|
||||
|
||||
## 🛠️ Tools
|
||||
|
||||
- [A Matrix server sanity tester including tests for proper MatrixRTC setup](https://codeberg.org/spaetz/testmatrix)
|
||||
|
||||
## 🤝 Want to Contribute?
|
||||
|
||||
Have a guide or blog post you'd like to share? Open a
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"error": {
|
||||
"call_is_not_supported": "Call is not supported",
|
||||
"call_not_found": "Call not found",
|
||||
"call_not_found_description": "<0>That link doesn't appear to belong to any existing call. Check that you have the right link, or <1>create a new one</1>.</0>",
|
||||
"call_not_found_description": "<0>That link doesn't appear to belong to any existing call. Check that you have the right link, or <2>create a new one</2>.</0>",
|
||||
"connection_lost": "Connection lost",
|
||||
"connection_lost_description": "You were disconnected from the call.",
|
||||
"e2ee_unsupported": "Incompatible browser",
|
||||
|
||||
16
package.json
16
package.json
@@ -8,7 +8,11 @@
|
||||
"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 .",
|
||||
@@ -54,7 +58,7 @@
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-visually-hidden": "^1.0.3",
|
||||
"@react-spring/web": "^9.4.4",
|
||||
"@react-spring/web": "^10.0.0",
|
||||
"@sentry/react": "^8.0.0",
|
||||
"@sentry/vite-plugin": "^3.0.0",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
@@ -70,8 +74,8 @@
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
@@ -83,6 +87,7 @@
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
@@ -113,11 +118,10 @@
|
||||
"posthog-js": "1.160.3",
|
||||
"prettier": "^3.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "18",
|
||||
"react-dom": "18",
|
||||
"react": "19",
|
||||
"react-dom": "19",
|
||||
"react-i18next": "^15.0.0",
|
||||
"react-router-dom": "^7.0.0",
|
||||
"react-use-clipboard": "^1.0.7",
|
||||
"react-use-measure": "^2.1.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sass": "^1.42.1",
|
||||
|
||||
@@ -7,6 +7,10 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const baseURL = process.env.USE_DOCKER
|
||||
? "http://localhost:8080"
|
||||
: "https://localhost:3000";
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
@@ -25,7 +29,7 @@ export default defineConfig({
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "https://localhost:3000",
|
||||
baseURL,
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
@@ -73,9 +77,13 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: "yarn dev",
|
||||
url: "https://localhost:3000",
|
||||
command: "./scripts/playwright-webserver-command.sh",
|
||||
url: baseURL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
ignoreHTTPSErrors: true,
|
||||
gracefulShutdown: {
|
||||
signal: "SIGTERM",
|
||||
timeout: 500,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,16 +69,27 @@ const CONFIG_JSON = {
|
||||
* Set the Element Call URL in the dev tool settings using `window.mxSettingsStore` via `page.evaluate`.
|
||||
* @param page
|
||||
*/
|
||||
async function setDevToolElementCallDevUrl(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
window.mxSettingsStore.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
"device",
|
||||
"https://localhost:3000/room",
|
||||
);
|
||||
});
|
||||
}
|
||||
const setDevToolElementCallDevUrl = process.env.USE_DOCKER
|
||||
? async (page: Page): Promise<void> => {
|
||||
await page.evaluate(() => {
|
||||
window.mxSettingsStore.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
"device",
|
||||
"http://localhost:8080/room",
|
||||
);
|
||||
});
|
||||
}
|
||||
: async (page: Page): Promise<void> => {
|
||||
await page.evaluate(() => {
|
||||
window.mxSettingsStore.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
"device",
|
||||
"https://localhost:3000/room",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a new user and returns page, clientHandle and mxId.
|
||||
|
||||
10
scripts/playwright-webserver-command.sh
Executable file
10
scripts/playwright-webserver-command.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
if [ -n "$USE_DOCKER" ]; then
|
||||
set -ex
|
||||
yarn build
|
||||
docker build -t element-call:testing .
|
||||
exec docker run --rm --name element-call-testing -p 8080:8080 -v ./config/config.devenv.json:/app/config.json:ro,Z element-call:testing
|
||||
else
|
||||
cp config/config.devenv.json public/config.json
|
||||
exec yarn dev
|
||||
fi
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
.error > h1 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error > p {
|
||||
|
||||
@@ -99,7 +99,7 @@ export const ErrorView: FC<Props> = ({
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<BigIcon className={styles.icon}>
|
||||
<Icon />
|
||||
<Icon aria-hidden />
|
||||
</BigIcon>
|
||||
<Heading as="h1" weight="semibold" size="md">
|
||||
{title}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
exports[`the content is rendered when the modal is open 1`] = `
|
||||
<div
|
||||
aria-labelledby="radix-:r4:"
|
||||
aria-labelledby="radix-«r4»"
|
||||
class="overlay animate modal dialog _glass_sepwu_8"
|
||||
data-state="open"
|
||||
id="radix-:r3:"
|
||||
id="radix-«r3»"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
@@ -18,7 +18,7 @@ exports[`the content is rendered when the modal is open 1`] = `
|
||||
>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
id="radix-:r4:"
|
||||
id="radix-«r4»"
|
||||
>
|
||||
My modal
|
||||
</h2>
|
||||
@@ -36,7 +36,7 @@ exports[`the content is rendered when the modal is open 1`] = `
|
||||
|
||||
exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
<div
|
||||
aria-labelledby="radix-:ra:"
|
||||
aria-labelledby="radix-«ra»"
|
||||
class="overlay modal drawer"
|
||||
data-state="open"
|
||||
data-vaul-animate="true"
|
||||
@@ -45,7 +45,7 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
data-vaul-drawer=""
|
||||
data-vaul-drawer-direction="bottom"
|
||||
data-vaul-snap-points="false"
|
||||
id="radix-:r9:"
|
||||
id="radix-«r9»"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
@@ -60,7 +60,7 @@ exports[`the modal renders as a drawer in mobile viewports 1`] = `
|
||||
class="handle"
|
||||
/>
|
||||
<h2
|
||||
id="radix-:ra:"
|
||||
id="radix-«ra»"
|
||||
style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
|
||||
>
|
||||
My modal
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
exports[`Toast > renders 1`] = `
|
||||
<button
|
||||
aria-labelledby="radix-:r4:"
|
||||
aria-labelledby="radix-«r4»"
|
||||
class="overlay animate toast"
|
||||
data-state="open"
|
||||
id="radix-:r3:"
|
||||
id="radix-«r3»"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
@@ -13,7 +13,7 @@ exports[`Toast > renders 1`] = `
|
||||
>
|
||||
<h3
|
||||
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36"
|
||||
id="radix-:r4:"
|
||||
id="radix-«r4»"
|
||||
>
|
||||
Hello world!
|
||||
</h3>
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`Can close reaction dialog 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":rb5:"
|
||||
aria-labelledby="«rb5»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -43,7 +43,7 @@ exports[`Can fully expand emoji picker 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r7m:"
|
||||
aria-labelledby="«r7m»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -74,7 +74,7 @@ exports[`Can lower hand 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r36:"
|
||||
aria-labelledby="«r36»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
@@ -108,7 +108,7 @@ exports[`Can open menu 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r0:"
|
||||
aria-labelledby="«r0»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -139,7 +139,7 @@ exports[`Can raise hand 1`] = `
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-labelledby=":r1j:"
|
||||
aria-labelledby="«r1j»"
|
||||
class="_button_vczzf_8 raisedButton _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface ConfigOptions {
|
||||
api_key: string;
|
||||
api_host: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The Sentry endpoint to which crash data will be sent.
|
||||
* This is only used in the full package of Element Call.
|
||||
@@ -22,6 +23,7 @@ export interface ConfigOptions {
|
||||
DSN: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The rageshake server to which feedback and debug logs will be sent.
|
||||
* This is only used in the full package of Element Call.
|
||||
@@ -66,6 +68,7 @@ export interface ConfigOptions {
|
||||
* Allow to join group calls without audio and video.
|
||||
*/
|
||||
feature_group_calls_without_video_and_audio?: boolean;
|
||||
|
||||
/**
|
||||
* Send device-specific call session membership state events instead of
|
||||
* legacy user-specific call membership state events.
|
||||
@@ -86,6 +89,7 @@ export interface ConfigOptions {
|
||||
* Defines whether participants should start with audio enabled by default.
|
||||
*/
|
||||
enable_audio?: boolean;
|
||||
|
||||
/**
|
||||
* Defines whether participants should start with video enabled by default.
|
||||
*/
|
||||
@@ -109,19 +113,38 @@ export interface ConfigOptions {
|
||||
* How long (in milliseconds) to wait before rotating end-to-end media encryption keys
|
||||
* when someone leaves a call.
|
||||
*/
|
||||
wait_for_key_rotation_ms?: number;
|
||||
/** @deprecated use wait_for_key_rotation_ms instead */
|
||||
key_rotation_on_leave_delay?: number;
|
||||
|
||||
/**
|
||||
* How often (in milliseconds) keep-alive messages should be sent to the server for
|
||||
* the MatrixRTC membership event.
|
||||
* The duration (in milliseconds) after the most recent keep-alive (delayed leave event restart)
|
||||
* that the server waits before sending the leave MatrixRTC membership event.
|
||||
*/
|
||||
delayed_leave_event_delay_ms?: number;
|
||||
/** @deprecated use delayed_leave_event_delay_ms instead */
|
||||
membership_server_side_expiry_timeout?: number;
|
||||
|
||||
/**
|
||||
* The time interval (in milliseconds) at which the client sends membership keep-alive
|
||||
* messages to the server by restarting the timer for the delayed leave event.
|
||||
*/
|
||||
delayed_leave_event_restart_ms?: number;
|
||||
/** @deprecated use delayed_leave_event_restart_ms instead */
|
||||
membership_keep_alive_period?: number;
|
||||
|
||||
/**
|
||||
* How long (in milliseconds) after the last keep-alive the server should expire the
|
||||
* MatrixRTC membership event.
|
||||
* How long we wait before retrying after a network error on any of the requests.
|
||||
*/
|
||||
membership_server_side_expiry_timeout?: number;
|
||||
network_error_retry_ms?: number;
|
||||
|
||||
/**
|
||||
* The timeout (in milliseconds) after we joined the call, that our membership should expire
|
||||
* unless we have explicitly updated it.
|
||||
*
|
||||
* This is what goes into the m.rtc.member event expiry field and is typically set to a number of hours.
|
||||
*/
|
||||
membership_event_expiry_ms?: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface Controls {
|
||||
/** @deprecated use onAudioDeviceSelect instead*/
|
||||
onOutputDeviceSelect?: (id: string) => void;
|
||||
onAudioDeviceSelect?: (id: string) => void;
|
||||
onAudioPlaybackStarted?: () => void;
|
||||
/** @deprecated use setAudioEnabled instead*/
|
||||
setOutputEnabled(enabled: boolean): void;
|
||||
setAudioEnabled(enabled: boolean): void;
|
||||
@@ -54,7 +55,13 @@ export const outputDevice$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
* This should also be used to display a darkened overlay screen letting the user know that audio is muted.
|
||||
*/
|
||||
export const setAudioEnabled$ = new Subject<boolean>();
|
||||
|
||||
let playbackStartedEmitted = false;
|
||||
export const setPlaybackStarted = (): void => {
|
||||
if (!playbackStartedEmitted) {
|
||||
playbackStartedEmitted = true;
|
||||
window.controls.onAudioPlaybackStarted?.();
|
||||
}
|
||||
};
|
||||
window.controls = {
|
||||
canEnterPip(): boolean {
|
||||
return setPipEnabled$.observed;
|
||||
|
||||
@@ -58,11 +58,13 @@ it("should render for member", () => {
|
||||
expect(container).toBeTruthy();
|
||||
expect(queryAllByTestId("audio")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should not render without member", () => {
|
||||
const memberships = [
|
||||
{ sender: "othermember", deviceId: "123" },
|
||||
] as CallMembership[];
|
||||
const { container, queryAllByTestId } = render(
|
||||
<MatrixAudioRenderer
|
||||
members={[{ sender: "othermember", deviceId: "123" }] as CallMembership[]}
|
||||
/>,
|
||||
<MatrixAudioRenderer members={memberships} />,
|
||||
);
|
||||
expect(container).toBeTruthy();
|
||||
expect(queryAllByTestId("audio")).toHaveLength(0);
|
||||
@@ -84,6 +86,7 @@ it("should not setup audioContext gain and pan if there is no need to.", () => {
|
||||
expect(testAudioContext.gain.gain.value).toEqual(1);
|
||||
expect(testAudioContext.pan.pan.value).toEqual(0);
|
||||
});
|
||||
|
||||
it("should setup audioContext gain and pan", () => {
|
||||
vi.spyOn(MediaDevicesContext, "useEarpieceAudioConfig").mockReturnValue({
|
||||
pan: 1,
|
||||
|
||||
@@ -14,11 +14,13 @@ import {
|
||||
type AudioTrackProps,
|
||||
} from "@livekit/components-react";
|
||||
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import { useEarpieceAudioConfig } from "./MediaDevicesContext";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import * as controls from "../controls";
|
||||
|
||||
const logger = rootLogger.getChild("[MatrixAudioRenderer]");
|
||||
export interface MatrixAudioRendererProps {
|
||||
/**
|
||||
* The list of participants to render audio for.
|
||||
@@ -59,6 +61,7 @@ export function MatrixAudioRenderer({
|
||||
);
|
||||
|
||||
const loggedInvalidIdentities = useRef(new Set<string>());
|
||||
|
||||
/**
|
||||
* Log an invalid livekit track identity.
|
||||
* A invalid identity is one that does not match any of the matrix rtc members.
|
||||
@@ -96,6 +99,14 @@ export function MatrixAudioRenderer({
|
||||
isValid
|
||||
);
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!tracks.some((t) => !validIdentities.has(t.participant.identity))) {
|
||||
logger.debug(
|
||||
`All audio tracks have a matching matrix call member identity.`,
|
||||
);
|
||||
loggedInvalidIdentities.current.clear();
|
||||
}
|
||||
}, [tracks, validIdentities]);
|
||||
|
||||
// This component is also (in addition to the "only play audio for connected members" logic above)
|
||||
// responsible for mimicking earpiece audio on iPhones.
|
||||
@@ -204,6 +215,7 @@ function AudioTrackWithAudioNodes({
|
||||
useContext ? [audioNodes.gain!, audioNodes.pan!] : [],
|
||||
);
|
||||
setTrackReady(true);
|
||||
controls.setPlaybackStarted();
|
||||
}, [audioContext, audioNodes, setTrackReady, trackReady, trackRef]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,14 @@ import {
|
||||
supportsBackgroundProcessors,
|
||||
type BackgroundOptions,
|
||||
} from "@livekit/track-processors";
|
||||
import { createContext, type FC, useContext, useEffect, useMemo } from "react";
|
||||
import {
|
||||
createContext,
|
||||
type FC,
|
||||
type JSX,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { type LocalVideoTrack } from "livekit-client";
|
||||
|
||||
import {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { useAudioContext } from "../useAudioContext";
|
||||
import { ActiveCall } from "./InCallView";
|
||||
import {
|
||||
flushPromises,
|
||||
mockEmitter,
|
||||
mockMatrixRoom,
|
||||
mockMatrixRoomMember,
|
||||
mockRtcMembership,
|
||||
@@ -129,6 +130,7 @@ function createGroupCallView(
|
||||
getMxcAvatarUrl: () => null,
|
||||
getCanonicalAlias: () => null,
|
||||
currentState: {
|
||||
...mockEmitter(),
|
||||
getJoinRule: () => JoinRule.Invite,
|
||||
} as Partial<RoomState> as RoomState,
|
||||
});
|
||||
|
||||
@@ -58,10 +58,9 @@ import { callEventAudioSounds } from "./CallEventAudioRenderer";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import {
|
||||
ConnectionLostError,
|
||||
E2EENotSupportedError,
|
||||
ElementCallError,
|
||||
ErrorCode,
|
||||
RTCSessionError,
|
||||
UnknownCallError,
|
||||
} from "../utils/errors.ts";
|
||||
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
|
||||
@@ -142,14 +141,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
useTypedEventEmitter(
|
||||
rtcSession,
|
||||
MatrixRTCSessionEvent.MembershipManagerError,
|
||||
(error) => {
|
||||
setExternalError(
|
||||
new RTCSessionError(
|
||||
ErrorCode.MEMBERSHIP_MANAGER_UNRECOVERABLE,
|
||||
error.message ?? error,
|
||||
),
|
||||
);
|
||||
},
|
||||
(error) => setExternalError(new ConnectionLostError()),
|
||||
);
|
||||
useEffect(() => {
|
||||
// Sanity check the room object
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
LinkIcon,
|
||||
CheckIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import useClipboard from "react-use-clipboard";
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
import { Modal } from "../Modal";
|
||||
import { getAbsoluteRoomUrl } from "../utils/matrix";
|
||||
@@ -42,18 +42,17 @@ export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
|
||||
() => getAbsoluteRoomUrl(room.roomId, e2eeSystem, room.name),
|
||||
[e2eeSystem, room.name, room.roomId],
|
||||
);
|
||||
const [, setCopied] = useClipboard(url);
|
||||
const [toastOpen, setToastOpen] = useState(false);
|
||||
const onToastDismiss = useCallback(() => setToastOpen(false), [setToastOpen]);
|
||||
|
||||
const onButtonClick = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setCopied();
|
||||
copy(url);
|
||||
onDismiss();
|
||||
setToastOpen(true);
|
||||
},
|
||||
[setCopied, onDismiss],
|
||||
[url, onDismiss],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -112,6 +112,7 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -262,6 +263,7 @@ exports[`should have a close button in widget mode 1`] = `
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -414,6 +416,7 @@ exports[`should render the error page with link back to home 1`] = `
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -566,6 +569,7 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -718,6 +722,7 @@ exports[`should report correct error for 'Connection lost' 1`] = `
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -868,6 +873,7 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1015,6 +1021,7 @@ exports[`should report correct error for 'Insufficient capacity' 1`] = `
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -46,7 +46,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-labelledby=":r0:"
|
||||
aria-labelledby="«r0»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -69,7 +69,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-labelledby=":r5:"
|
||||
aria-labelledby="«r5»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -91,7 +91,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby=":ra:"
|
||||
aria-labelledby="«ra»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
@@ -112,7 +112,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby=":rf:"
|
||||
aria-labelledby="«rf»"
|
||||
class="_button_vczzf_8 endCall _has-icon_vczzf_57 _icon-only_vczzf_50 _destructive_vczzf_107"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -138,7 +138,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
class="toggle layout"
|
||||
>
|
||||
<input
|
||||
aria-labelledby=":rk:"
|
||||
aria-labelledby="«rk»"
|
||||
name="layout"
|
||||
type="radio"
|
||||
value="spotlight"
|
||||
@@ -156,7 +156,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
aria-labelledby=":rp:"
|
||||
aria-labelledby="«rp»"
|
||||
checked=""
|
||||
name="layout"
|
||||
type="radio"
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
type MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef } 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
|
||||
@@ -22,38 +24,27 @@ import { type LivekitFocus, isLivekitFocus } from "matrix-js-sdk/lib/matrixrtc";
|
||||
export function useActiveLivekitFocus(
|
||||
rtcSession: MatrixRTCSession,
|
||||
): LivekitFocus | undefined {
|
||||
const [activeFocus, setActiveFocus] = useState(() => {
|
||||
const f = rtcSession.getActiveFocus();
|
||||
// Only handle foci with type="livekit" for now.
|
||||
return !!f && isLivekitFocus(f) ? f : 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 onMembershipsChanged = useCallback(() => {
|
||||
const newActiveFocus = rtcSession.getActiveFocus();
|
||||
if (!!newActiveFocus && !isLivekitFocus(newActiveFocus)) return;
|
||||
if (!deepCompare(activeFocus, newActiveFocus)) {
|
||||
const prevActiveFocus = useRef(activeFocus);
|
||||
useEffect(() => {
|
||||
if (!deepCompare(activeFocus, prevActiveFocus.current)) {
|
||||
const oldestMembership = rtcSession.getOldestMembership();
|
||||
logger.warn(
|
||||
`Got new active focus from membership: ${oldestMembership?.sender}/${oldestMembership?.deviceId}.
|
||||
Updating focus (focus switch) from ${JSON.stringify(activeFocus)} to ${JSON.stringify(newActiveFocus)}`,
|
||||
Updated focus (focus switch) from ${JSON.stringify(prevActiveFocus.current)} to ${JSON.stringify(activeFocus)}`,
|
||||
);
|
||||
setActiveFocus(newActiveFocus);
|
||||
prevActiveFocus.current = activeFocus;
|
||||
}
|
||||
}, [activeFocus, rtcSession]);
|
||||
|
||||
useEffect(() => {
|
||||
rtcSession.on(
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
onMembershipsChanged,
|
||||
);
|
||||
|
||||
return (): void => {
|
||||
rtcSession.off(
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
onMembershipsChanged,
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
return activeFocus;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
/*
|
||||
Copyright 2023, 2024 New Vector Ltd.
|
||||
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 { type Room, RoomEvent } from "matrix-js-sdk";
|
||||
import { useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useTypedEventEmitter } from "../useEvents";
|
||||
import { useTypedEventEmitterState } from "../useEvents";
|
||||
|
||||
export function useRoomName(room: Room): string {
|
||||
const [, setNumUpdates] = useState(0);
|
||||
// Whenever the name changes, force an update
|
||||
useTypedEventEmitter(room, RoomEvent.Name, () => setNumUpdates((n) => n + 1));
|
||||
return room.name;
|
||||
return useTypedEventEmitterState(
|
||||
room,
|
||||
RoomEvent.Name,
|
||||
useCallback(() => room.name, [room]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { type RoomState, RoomStateEvent, type Room } from "matrix-js-sdk";
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
type RoomState,
|
||||
RoomStateEvent,
|
||||
type Room,
|
||||
RoomEvent,
|
||||
} from "matrix-js-sdk";
|
||||
|
||||
import { useTypedEventEmitter } from "../useEvents";
|
||||
import { useTypedEventEmitterState } from "../useEvents";
|
||||
|
||||
/**
|
||||
* A React hook for values computed from room state.
|
||||
@@ -16,14 +21,17 @@ import { useTypedEventEmitter } from "../useEvents";
|
||||
* @param f A mapping from the current room state to the computed value.
|
||||
* @returns The computed value.
|
||||
*/
|
||||
export const useRoomState = <T>(room: Room, f: (state: RoomState) => T): T => {
|
||||
const [numUpdates, setNumUpdates] = useState(0);
|
||||
useTypedEventEmitter(
|
||||
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(
|
||||
room,
|
||||
RoomStateEvent.Update,
|
||||
useCallback(() => setNumUpdates((n) => n + 1), [setNumUpdates]),
|
||||
RoomEvent.CurrentStateUpdated,
|
||||
useCallback(() => room.currentState, [room]),
|
||||
);
|
||||
// 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]);
|
||||
};
|
||||
return useTypedEventEmitterState(
|
||||
currentState,
|
||||
RoomStateEvent.Update,
|
||||
useCallback(() => f(currentState), [f, currentState]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,10 +121,18 @@ export async function enterRTCSession(
|
||||
...(useDeviceSessionMemberEvents !== undefined && {
|
||||
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
|
||||
}),
|
||||
delayedLeaveEventRestartMs:
|
||||
matrixRtcSessionConfig?.delayed_leave_event_restart_ms ??
|
||||
matrixRtcSessionConfig?.membership_keep_alive_period,
|
||||
delayedLeaveEventDelayMs:
|
||||
matrixRtcSessionConfig?.delayed_leave_event_delay_ms ??
|
||||
matrixRtcSessionConfig?.membership_server_side_expiry_timeout,
|
||||
networkErrorRetryMs: matrixRtcSessionConfig?.membership_keep_alive_period,
|
||||
makeKeyDelay: matrixRtcSessionConfig?.key_rotation_on_leave_delay,
|
||||
networkErrorRetryMs: matrixRtcSessionConfig?.network_error_retry_ms,
|
||||
makeKeyDelay:
|
||||
matrixRtcSessionConfig?.wait_for_key_rotation_ms ??
|
||||
matrixRtcSessionConfig?.key_rotation_on_leave_delay,
|
||||
membershipEventExpiryMs:
|
||||
matrixRtcSessionConfig?.membership_event_expiry_ms,
|
||||
useExperimentalToDeviceTransport,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "./livekit/MediaDevicesContext";
|
||||
import { type PrefetchedSounds } from "./soundUtils";
|
||||
import { useUrlParams } from "./UrlParams";
|
||||
import * as controls from "./controls";
|
||||
|
||||
/**
|
||||
* Play a sound though a given AudioContext. Will take
|
||||
@@ -42,6 +43,7 @@ async function playSound(
|
||||
src.buffer = buffer;
|
||||
src.connect(gain).connect(pan).connect(ctx.destination);
|
||||
const p = new Promise<void>((r) => src.addEventListener("ended", () => r()));
|
||||
controls.setPlaybackStarted();
|
||||
src.start();
|
||||
return p;
|
||||
}
|
||||
|
||||
88
src/useEvents.test.tsx
Normal file
88
src/useEvents.test.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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");
|
||||
});
|
||||
@@ -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 { useEffect } from "react";
|
||||
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
||||
|
||||
import type {
|
||||
Listener,
|
||||
@@ -13,7 +13,9 @@ 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,
|
||||
@@ -33,7 +35,9 @@ 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>,
|
||||
@@ -50,3 +54,33 @@ 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);
|
||||
}
|
||||
|
||||
51
src/useLocalStorage.test.tsx
Normal file
51
src/useLocalStorage.test.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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");
|
||||
});
|
||||
@@ -5,50 +5,44 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { TypedEventEmitter } from "matrix-js-sdk";
|
||||
|
||||
import { useTypedEventEmitterState } from "./useEvents";
|
||||
|
||||
type LocalStorageItem = ReturnType<typeof localStorage.getItem>;
|
||||
|
||||
// Bus to notify other useLocalStorage consumers when an item is changed
|
||||
export const localStorageBus = new EventEmitter();
|
||||
export const localStorageBus = new TypedEventEmitter<
|
||||
string,
|
||||
{ [key: string]: () => void }
|
||||
>();
|
||||
|
||||
/**
|
||||
* 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 const useLocalStorage = (
|
||||
export function useLocalStorage(
|
||||
key: string,
|
||||
): [LocalStorageItem, (value: string) => void] => {
|
||||
const [value, setValue] = useState<LocalStorageItem>(() =>
|
||||
localStorage.getItem(key),
|
||||
): [LocalStorageItem, (value: string) => void] {
|
||||
const value = useTypedEventEmitterState(
|
||||
localStorageBus,
|
||||
key,
|
||||
useCallback(() => localStorage.getItem(key), [key]),
|
||||
);
|
||||
const setValue = useCallback(
|
||||
(newValue: string) => setLocalStorageItemReactive(key, newValue),
|
||||
[key],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
localStorageBus.on(key, setValue);
|
||||
return (): void => {
|
||||
localStorageBus.off(key, setValue);
|
||||
};
|
||||
}, [key, setValue]);
|
||||
|
||||
return [
|
||||
value,
|
||||
useCallback(
|
||||
(newValue: string) => {
|
||||
setValue(newValue);
|
||||
localStorage.setItem(key, newValue);
|
||||
localStorageBus.emit(key, newValue);
|
||||
},
|
||||
[key, setValue],
|
||||
),
|
||||
];
|
||||
};
|
||||
return [value, setValue];
|
||||
}
|
||||
|
||||
export const setLocalStorageItemReactive = (
|
||||
key: string,
|
||||
value: string,
|
||||
): void => {
|
||||
localStorage.setItem(key, value);
|
||||
localStorageBus.emit(key, value);
|
||||
localStorageBus.emit(key);
|
||||
};
|
||||
|
||||
@@ -10,33 +10,31 @@ import {
|
||||
type MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TypedEventEmitter } from "matrix-js-sdk";
|
||||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { useTypedEventEmitterState } from "./useEvents";
|
||||
|
||||
const dummySession = new TypedEventEmitter();
|
||||
|
||||
export function useMatrixRTCSessionJoinState(
|
||||
rtcSession: MatrixRTCSession | undefined,
|
||||
): boolean {
|
||||
const [, setNumUpdates] = useState(0);
|
||||
// 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]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
logger.info(
|
||||
`Session in room ${rtcSession?.room.roomId} changed to ${
|
||||
isJoined ? "joined" : "left"
|
||||
}`,
|
||||
);
|
||||
}, [rtcSession, isJoined]);
|
||||
|
||||
return (): void => {
|
||||
rtcSession.off(
|
||||
MatrixRTCSessionEvent.JoinStateChanged,
|
||||
onJoinStateChanged,
|
||||
);
|
||||
};
|
||||
}
|
||||
}, [rtcSession]);
|
||||
|
||||
return rtcSession?.isJoined() ?? false;
|
||||
return isJoined;
|
||||
}
|
||||
|
||||
@@ -5,39 +5,21 @@ 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, useEffect, useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useTypedEventEmitterState } from "./useEvents";
|
||||
|
||||
export function useMatrixRTCSessionMemberships(
|
||||
rtcSession: MatrixRTCSession,
|
||||
): CallMembership[] {
|
||||
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;
|
||||
return useTypedEventEmitterState(
|
||||
rtcSession,
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
useCallback(() => rtcSession.memberships, [rtcSession]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/*
|
||||
Copyright 2023, 2024 New Vector Ltd.
|
||||
Copyright 2023-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 { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { useEventTarget } from "./useEvents";
|
||||
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
||||
|
||||
/**
|
||||
* React hook that tracks whether the given media query matches.
|
||||
@@ -15,14 +13,13 @@ import { useEventTarget } from "./useEvents";
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const mediaQuery = useMemo(() => window.matchMedia(query), [query]);
|
||||
|
||||
const [numChanges, setNumChanges] = useState(0);
|
||||
useEventTarget(
|
||||
mediaQuery,
|
||||
"change",
|
||||
useCallback(() => setNumChanges((n) => n + 1), [setNumChanges]),
|
||||
const subscribe = useCallback(
|
||||
(onChange: () => void) => {
|
||||
mediaQuery.addEventListener("change", onChange);
|
||||
return (): void => mediaQuery.removeEventListener("change", onChange);
|
||||
},
|
||||
[mediaQuery],
|
||||
);
|
||||
|
||||
// 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]);
|
||||
const getState = useCallback(() => mediaQuery.matches, [mediaQuery]);
|
||||
return useSyncExternalStore(subscribe, getState);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ export enum ErrorCode {
|
||||
*/
|
||||
MISSING_MATRIX_RTC_FOCUS = "MISSING_MATRIX_RTC_FOCUS",
|
||||
CONNECTION_LOST_ERROR = "CONNECTION_LOST_ERROR",
|
||||
MEMBERSHIP_MANAGER_UNRECOVERABLE = "MEMBERSHIP_MANAGER_UNRECOVERABLE",
|
||||
/** LiveKit indicates that the server has hit its track limits */
|
||||
INSUFFICIENT_CAPACITY_ERROR = "INSUFFICIENT_CAPACITY_ERROR",
|
||||
E2EE_NOT_SUPPORTED = "E2EE_NOT_SUPPORTED",
|
||||
@@ -25,7 +24,6 @@ export enum ErrorCategory {
|
||||
/** Calling is not supported, server misconfigured (JWT service missing, no MSC support ...)*/
|
||||
CONFIGURATION_ISSUE = "CONFIGURATION_ISSUE",
|
||||
NETWORK_CONNECTIVITY = "NETWORK_CONNECTIVITY",
|
||||
RTC_SESSION_FAILURE = "RTC_SESSION_FAILURE",
|
||||
CLIENT_CONFIGURATION = "CLIENT_CONFIGURATION",
|
||||
UNKNOWN = "UNKNOWN",
|
||||
// SYSTEM_FAILURE / FEDERATION_FAILURE ..
|
||||
@@ -84,11 +82,6 @@ export class ConnectionLostError extends ElementCallError {
|
||||
}
|
||||
}
|
||||
|
||||
export class RTCSessionError extends ElementCallError {
|
||||
public constructor(code: ErrorCode, message: string) {
|
||||
super("RTCSession Error", code, ErrorCategory.RTC_SESSION_FAILURE, message);
|
||||
}
|
||||
}
|
||||
export class E2EENotSupportedError extends ElementCallError {
|
||||
public constructor() {
|
||||
super(
|
||||
@@ -106,7 +99,7 @@ export class UnknownCallError extends ElementCallError {
|
||||
t("error.generic"),
|
||||
ErrorCode.UNKNOWN_ERROR,
|
||||
ErrorCategory.UNKNOWN,
|
||||
error.message,
|
||||
undefined,
|
||||
// Properly set it as a cause for a better reporting on sentry
|
||||
error,
|
||||
);
|
||||
|
||||
@@ -108,7 +108,7 @@ interface EmitterMock<T> {
|
||||
removeListener: () => T;
|
||||
}
|
||||
|
||||
function mockEmitter<T>(): EmitterMock<T> {
|
||||
export function mockEmitter<T>(): EmitterMock<T> {
|
||||
return {
|
||||
on(): T {
|
||||
return this as T;
|
||||
|
||||
@@ -18,9 +18,9 @@ export default defineConfig((env) =>
|
||||
output: "./config.json",
|
||||
data: {
|
||||
matrix_rtc_session: {
|
||||
key_rotation_on_leave_delay: 15000,
|
||||
membership_keep_alive_period: 5000,
|
||||
membership_server_side_expiry_timeout: 15000,
|
||||
wait_for_key_rotation_ms: 5000,
|
||||
delayed_leave_event_restart_ms: 4000,
|
||||
delayed_leave_event_delay_ms: 18000,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -89,6 +89,7 @@ export default defineConfig(({ mode, packageType }) => {
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: mode === "production" ? true : false,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
|
||||
187
yarn.lock
187
yarn.lock
@@ -2780,13 +2780,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@matrix-org/olm@npm:3.2.15":
|
||||
version: 3.2.15
|
||||
resolution: "@matrix-org/olm@npm:3.2.15"
|
||||
checksum: 10c0/82a40d6e4e632a90670d4f15e8962272e302f4b9deed4fc78455c5ca78422c13bde6b53ebfc406630336926c9574386c9d9069c9c023db1c3d143117985c1e50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mediapipe/tasks-vision@npm:^0.10.18":
|
||||
version: 0.10.21
|
||||
resolution: "@mediapipe/tasks-vision@npm:0.10.21"
|
||||
@@ -4391,69 +4384,69 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/animated@npm:~9.7.5":
|
||||
version: 9.7.5
|
||||
resolution: "@react-spring/animated@npm:9.7.5"
|
||||
"@react-spring/animated@npm:~10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/animated@npm:10.0.1"
|
||||
dependencies:
|
||||
"@react-spring/shared": "npm:~9.7.5"
|
||||
"@react-spring/types": "npm:~9.7.5"
|
||||
"@react-spring/shared": "npm:~10.0.1"
|
||||
"@react-spring/types": "npm:~10.0.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/f8c2473c60f39a878c7dd0fdfcfcdbc720521e1506aa3f63c9de64780694a0a73d5ccc535a5ccec3520ddb70a71cf43b038b32c18e99531522da5388c510ecd7
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/aaccd4a8b0280ac846d463b253ad8f092ee4afc9dbedc8e77616adf5399ffec755344f09fdd8487cadaf815840dff84d354d1143579c27c2fcd6937549b5fc40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/core@npm:~9.7.5":
|
||||
version: 9.7.5
|
||||
resolution: "@react-spring/core@npm:9.7.5"
|
||||
"@react-spring/core@npm:~10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/core@npm:10.0.1"
|
||||
dependencies:
|
||||
"@react-spring/animated": "npm:~9.7.5"
|
||||
"@react-spring/shared": "npm:~9.7.5"
|
||||
"@react-spring/types": "npm:~9.7.5"
|
||||
"@react-spring/animated": "npm:~10.0.1"
|
||||
"@react-spring/shared": "npm:~10.0.1"
|
||||
"@react-spring/types": "npm:~10.0.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/5bfd83dfe248cd91889f215f015d908c7714ef445740fd5afa054b27ebc7d5a456abf6c309e2459d9b5b436e78d6fda16b62b9601f96352e9130552c02270830
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/059b122dda4138e5e7e461abd49350921e326735ca9a1d8aa19b1fdbae0937661b5f71af6fe82fd8f59e8db5549627849b38cc3f7ef2ec7ee9c93c3d6225174f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/rafz@npm:~9.7.5":
|
||||
version: 9.7.5
|
||||
resolution: "@react-spring/rafz@npm:9.7.5"
|
||||
checksum: 10c0/8bdad180feaa9a0e870a513043a5e98a4e9b7292a9f887575b7e6fadab2677825bc894b7ff16c38511b35bfe6cc1072df5851c5fee64448d67551559578ca759
|
||||
"@react-spring/rafz@npm:~10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/rafz@npm:10.0.1"
|
||||
checksum: 10c0/cba76f143d3a06f79dd0c09f7aefd17df9cca9b2c1ef7f9103255e5351326f4a42a5a1366f731a78f74380d96ba683bcc2a49312ed1e4b9e9e249e72c9ff68cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/shared@npm:~9.7.5":
|
||||
version: 9.7.5
|
||||
resolution: "@react-spring/shared@npm:9.7.5"
|
||||
"@react-spring/shared@npm:~10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/shared@npm:10.0.1"
|
||||
dependencies:
|
||||
"@react-spring/rafz": "npm:~9.7.5"
|
||||
"@react-spring/types": "npm:~9.7.5"
|
||||
"@react-spring/rafz": "npm:~10.0.1"
|
||||
"@react-spring/types": "npm:~10.0.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/0207eacccdedd918a2fc55e78356ce937f445ce27ad9abd5d3accba8f9701a39349b55115641dc2b39bb9d3a155b058c185b411d292dc8cc5686bfa56f73b94f
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/f056aaa018b3744afd8244e8eea24534d32f92fad9ace815b80e159b296fb5db148e2c9bd840ad9a5188e7a3c0778fd564b8af9ae02cd500e019a228398fb3cf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/types@npm:~9.7.5":
|
||||
version: 9.7.5
|
||||
resolution: "@react-spring/types@npm:9.7.5"
|
||||
checksum: 10c0/85c05121853cacb64f7cf63a4855e9044635e1231f70371cd7b8c78bc10be6f4dd7c68f592f92a2607e8bb68051540989b4677a2ccb525dba937f5cd95dc8bc1
|
||||
"@react-spring/types@npm:~10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/types@npm:10.0.1"
|
||||
checksum: 10c0/260890f9c156dc69b77c846510017156d8c0a07cce70edc7c108e57b0cf4122b26a15e724b191481a51b2c914296de9e81d56618b2c339339d4b221930691baa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spring/web@npm:^9.4.4":
|
||||
version: 9.7.5
|
||||
resolution: "@react-spring/web@npm:9.7.5"
|
||||
"@react-spring/web@npm:^10.0.0":
|
||||
version: 10.0.1
|
||||
resolution: "@react-spring/web@npm:10.0.1"
|
||||
dependencies:
|
||||
"@react-spring/animated": "npm:~9.7.5"
|
||||
"@react-spring/core": "npm:~9.7.5"
|
||||
"@react-spring/shared": "npm:~9.7.5"
|
||||
"@react-spring/types": "npm:~9.7.5"
|
||||
"@react-spring/animated": "npm:~10.0.1"
|
||||
"@react-spring/core": "npm:~10.0.1"
|
||||
"@react-spring/shared": "npm:~10.0.1"
|
||||
"@react-spring/types": "npm:~10.0.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/bcd1e052e1b16341a12a19bf4515f153ca09d1fa86ff7752a5d02d7c4db58e8baf80e6283e64411f1e388c65340dce2254b013083426806b5dbae38bd151e53e
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/a0c788c9fd881ccb834feb22fc0694e74e59f7b76e498f0096f5b65e2c9812513955bf45ee27d7c5348f56a7bba7c5a6961d4be663728bb2172fe5aa6b6bdfc4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5224,13 +5217,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/prop-types@npm:*":
|
||||
version: 15.7.14
|
||||
resolution: "@types/prop-types@npm:15.7.14"
|
||||
checksum: 10c0/1ec775160bfab90b67a782d735952158c7e702ca4502968aa82565bd8e452c2de8601c8dfe349733073c31179116cf7340710160d3836aa8a1ef76d1532893b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/qrcode@npm:^1.5.5":
|
||||
version: 1.5.5
|
||||
resolution: "@types/qrcode@npm:1.5.5"
|
||||
@@ -5240,22 +5226,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-dom@npm:^18.3.0":
|
||||
version: 18.3.7
|
||||
resolution: "@types/react-dom@npm:18.3.7"
|
||||
"@types/react-dom@npm:^19.0.0":
|
||||
version: 19.1.5
|
||||
resolution: "@types/react-dom@npm:19.1.5"
|
||||
peerDependencies:
|
||||
"@types/react": ^18.0.0
|
||||
checksum: 10c0/8bd309e2c3d1604a28a736a24f96cbadf6c05d5288cfef8883b74f4054c961b6b3a5e997fd5686e492be903c8f3380dba5ec017eff3906b1256529cd2d39603e
|
||||
"@types/react": ^19.0.0
|
||||
checksum: 10c0/2a29e77cf6bb6e9f57bcfa54509c216cad2e16e244f0bd56369966ec88c072b9c91f6011d14f9e18fbfe2b801b18b86f616de75e5c8aef0be73c1f74abb33b49
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:^18.3.0":
|
||||
version: 18.3.23
|
||||
resolution: "@types/react@npm:18.3.23"
|
||||
"@types/react@npm:^19.0.0":
|
||||
version: 19.1.6
|
||||
resolution: "@types/react@npm:19.1.6"
|
||||
dependencies:
|
||||
"@types/prop-types": "npm:*"
|
||||
csstype: "npm:^3.0.2"
|
||||
checksum: 10c0/49331800b76572eb2992a5c44801dbf8c612a5f99c8f4e4200f06c7de6f3a6e9455c661784a6c5469df96fa45622cb4a9d0982c44e6a0d5719be5f2ef1f545ed
|
||||
checksum: 10c0/8b10b198e28997b3c57559750f8bcf5ae7b33c554b16b6f4fe2ece1d4de6a2fc8cb53e7effe08ec9cb939d2f479eb97c5e08aac2cf83b10a90164fe451cc8ea2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6867,7 +6852,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"copy-to-clipboard@npm:^3.3.1":
|
||||
"copy-to-clipboard@npm:^3.3.3":
|
||||
version: 3.3.3
|
||||
resolution: "copy-to-clipboard@npm:3.3.3"
|
||||
dependencies:
|
||||
@@ -7457,7 +7442,7 @@ __metadata:
|
||||
"@radix-ui/react-dialog": "npm:^1.0.4"
|
||||
"@radix-ui/react-slider": "npm:^1.1.2"
|
||||
"@radix-ui/react-visually-hidden": "npm:^1.0.3"
|
||||
"@react-spring/web": "npm:^9.4.4"
|
||||
"@react-spring/web": "npm:^10.0.0"
|
||||
"@sentry/react": "npm:^8.0.0"
|
||||
"@sentry/vite-plugin": "npm:^3.0.0"
|
||||
"@stylistic/eslint-plugin": "npm:^3.0.0"
|
||||
@@ -7473,8 +7458,8 @@ __metadata:
|
||||
"@types/node": "npm:^22.0.0"
|
||||
"@types/pako": "npm:^2.0.3"
|
||||
"@types/qrcode": "npm:^1.5.5"
|
||||
"@types/react": "npm:^18.3.0"
|
||||
"@types/react-dom": "npm:^18.3.0"
|
||||
"@types/react": "npm:^19.0.0"
|
||||
"@types/react-dom": "npm:^19.0.0"
|
||||
"@types/sdp-transform": "npm:^2.4.5"
|
||||
"@types/uuid": "npm:10"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.31.0"
|
||||
@@ -7486,6 +7471,7 @@ __metadata:
|
||||
"@vitest/coverage-v8": "npm:^3.0.0"
|
||||
babel-plugin-transform-vite-meta-env: "npm:^1.0.3"
|
||||
classnames: "npm:^2.3.1"
|
||||
copy-to-clipboard: "npm:^3.3.3"
|
||||
eslint: "npm:^8.14.0"
|
||||
eslint-config-google: "npm:^0.14.0"
|
||||
eslint-config-prettier: "npm:^10.0.0"
|
||||
@@ -7516,11 +7502,10 @@ __metadata:
|
||||
posthog-js: "npm:1.160.3"
|
||||
prettier: "npm:^3.0.0"
|
||||
qrcode: "npm:^1.5.4"
|
||||
react: "npm:18"
|
||||
react-dom: "npm:18"
|
||||
react: "npm:19"
|
||||
react-dom: "npm:19"
|
||||
react-i18next: "npm:^15.0.0"
|
||||
react-router-dom: "npm:^7.0.0"
|
||||
react-use-clipboard: "npm:^1.0.7"
|
||||
react-use-measure: "npm:^2.1.1"
|
||||
rxjs: "npm:^7.8.1"
|
||||
sass: "npm:^1.42.1"
|
||||
@@ -9922,8 +9907,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"livekit-client@npm:^2.13.0":
|
||||
version: 2.13.3
|
||||
resolution: "livekit-client@npm:2.13.3"
|
||||
version: 2.13.4
|
||||
resolution: "livekit-client@npm:2.13.4"
|
||||
dependencies:
|
||||
"@livekit/mutex": "npm:1.1.1"
|
||||
"@livekit/protocol": "npm:1.38.0"
|
||||
@@ -9936,7 +9921,7 @@ __metadata:
|
||||
webrtc-adapter: "npm:^9.0.1"
|
||||
peerDependencies:
|
||||
"@types/dom-mediacapture-record": ^1
|
||||
checksum: 10c0/9eadb9835514551c8e834cebb607999e632bc4b3b3e5d5e206db90b66e2e5467b8a1f76a03c648e00cc96c87256b4eece58dbb2b92dfcafa84f4798bbb305060
|
||||
checksum: 10c0/0d265e03f3ec50ab847a6127e2a3af4701d4d86feb614bac08a7b0e847f122ddd571ed5a90240a7ac0fbc10d025804e94a0919c1705fc93b3091ff3991f58a0f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10007,7 +9992,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
|
||||
"loose-envify@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "loose-envify@npm:1.4.0"
|
||||
dependencies:
|
||||
@@ -10148,12 +10133,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=develop":
|
||||
version: 37.6.0
|
||||
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=bf6dc16ad32d47f2c6e167236f4c853ceef01d4f"
|
||||
version: 37.8.0
|
||||
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=adaf92162377414df620e004d8c58c67118490ca"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.12.5"
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "npm:^14.2.0"
|
||||
"@matrix-org/olm": "npm:3.2.15"
|
||||
another-json: "npm:^0.2.0"
|
||||
bs58: "npm:^6.0.0"
|
||||
content-type: "npm:^1.0.4"
|
||||
@@ -10166,7 +10150,7 @@ __metadata:
|
||||
sdp-transform: "npm:^2.14.1"
|
||||
unhomoglyph: "npm:^1.0.6"
|
||||
uuid: "npm:11"
|
||||
checksum: 10c0/2877e7c5b2779200b48f3152bb7b510e58899b1e779e7e6d2bc1a236ed178fa8858f0547b644a2029f48f55dc7cc3954f48e2598c8963d45293c8280ccc23039
|
||||
checksum: 10c0/8e842056ff926c615d53a4a333c9e54d0fbe7f88b97b55629464e67850cc512606accde6a8724dd48ac82714483f685515913b5556e9824c4d63c443317be1ef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11577,15 +11561,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dom@npm:18":
|
||||
version: 18.3.1
|
||||
resolution: "react-dom@npm:18.3.1"
|
||||
"react-dom@npm:19":
|
||||
version: 19.1.0
|
||||
resolution: "react-dom@npm:19.1.0"
|
||||
dependencies:
|
||||
loose-envify: "npm:^1.1.0"
|
||||
scheduler: "npm:^0.23.2"
|
||||
scheduler: "npm:^0.26.0"
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
checksum: 10c0/a752496c1941f958f2e8ac56239172296fcddce1365ce45222d04a1947e0cc5547df3e8447f855a81d6d39f008d7c32eab43db3712077f09e3f67c4874973e85
|
||||
react: ^19.1.0
|
||||
checksum: 10c0/3e26e89bb6c67c9a6aa86cb888c7a7f8258f2e347a6d2a15299c17eb16e04c19194e3452bc3255bd34000a61e45e2cb51e46292392340432f133e5a5d2dfb5fc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11729,18 +11712,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-use-clipboard@npm:^1.0.7":
|
||||
version: 1.0.9
|
||||
resolution: "react-use-clipboard@npm:1.0.9"
|
||||
dependencies:
|
||||
copy-to-clipboard: "npm:^3.3.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
react-dom: ^16.8.0 || ^17 || ^18
|
||||
checksum: 10c0/f75ba85b885ae45d0c99a09a0652a2f64c1d9d6d015293bf01b2e6a9f50ff2a6b1830f6e60c1f639b9ae33fa434bb31adbf43f613df2402438204ea7456cf86a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-use-measure@npm:^2.1.1":
|
||||
version: 2.1.7
|
||||
resolution: "react-use-measure@npm:2.1.7"
|
||||
@@ -11754,12 +11725,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react@npm:18":
|
||||
version: 18.3.1
|
||||
resolution: "react@npm:18.3.1"
|
||||
dependencies:
|
||||
loose-envify: "npm:^1.1.0"
|
||||
checksum: 10c0/283e8c5efcf37802c9d1ce767f302dd569dd97a70d9bb8c7be79a789b9902451e0d16334b05d73299b20f048cbc3c7d288bbbde10b701fa194e2089c237dbea3
|
||||
"react@npm:19":
|
||||
version: 19.1.0
|
||||
resolution: "react@npm:19.1.0"
|
||||
checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -12352,12 +12321,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"scheduler@npm:^0.23.2":
|
||||
version: 0.23.2
|
||||
resolution: "scheduler@npm:0.23.2"
|
||||
dependencies:
|
||||
loose-envify: "npm:^1.1.0"
|
||||
checksum: 10c0/26383305e249651d4c58e6705d5f8425f153211aef95f15161c151f7b8de885f24751b377e4a0b3dd42cce09aad3f87a61dab7636859c0d89b7daf1a1e2a5c78
|
||||
"scheduler@npm:^0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "scheduler@npm:0.26.0"
|
||||
checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user