diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index d8af6f92..05ee362b 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -2,11 +2,15 @@ name: Test
on:
pull_request: {}
push:
- branches: [livekit, full-mesh]
+ branches: [livekit]
jobs:
vitest:
name: Run unit tests
runs-on: ubuntu-latest
+ container:
+ # Make sure to grab the latest version of the Playwright image
+ # https://playwright.dev/docs/docker#pull-the-image
+ image: mcr.microsoft.com/playwright:v1.60.0-noble
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
diff --git a/.storybook/main.ts b/.storybook/main.ts
index 977eca73..e227ef76 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -9,7 +9,28 @@ import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
- addons: ["@storybook/addon-docs"],
+ addons: ["@storybook/addon-docs", "@storybook/addon-vitest"],
framework: "@storybook/react-vite",
+ // THIS IS IMPORTANT
+ // vitest runs without Vite's normal dependency optimization, so we need to manually include the polyfills for the stories to work.
+ // otherwise we will get: new dependencies optimized: ...
+ // and
+ // ```
+ // [vitest] Vite unexpectedly reloaded a test. This may cause tests to fail, lead to flaky behaviour or duplicated test runs.
+ // For a stable experience, please add mentioned dependencies to your config's `optimizeDeps.include` field manually.
+ // ```
+ // which breaks the storybook ci on the first and only run.
+ viteFinal(config) {
+ config.optimizeDeps = {
+ ...config.optimizeDeps,
+ include: [
+ ...(config.optimizeDeps?.include ?? []),
+ "vite-plugin-node-polyfills/shims/buffer",
+ "vite-plugin-node-polyfills/shims/global",
+ "vite-plugin-node-polyfills/shims/process",
+ ],
+ };
+ return config;
+ },
};
export default config;
diff --git a/README.md b/README.md
index 31253cda..5df2c42e 100644
--- a/README.md
+++ b/README.md
@@ -177,6 +177,13 @@ discuss and coordinate translation efforts.
## 🛠️ Development
+### Dependencies
+
+- Node.js (e.g. via [nvm](https://github.com/nvm-sh/nvm))
+- [Corepack](https://github.com/nodejs/corepack) (not bundled with Node.js anymore starting from 25.0.0)
+- Docker client and runtime + Docker Compose (for the backend)
+ - On macOS you can install everything with `brew install colima docker docker-compose`
+
### Frontend
To get started clone and set up this project:
@@ -219,14 +226,15 @@ including federation:
- Minimum TLS reverse proxy for
- Synapse homeserver: `synapse.m.localhost` and `synapse.othersite.m.localhost`
- MatrixRTC backend: `matrix-rtc.m.localhost` and `matrix-rtc.othersite.m.localhost`
- - Local Element Call development `call.m.localhost` via `yarn dev --host `
+ - Local Element Call development `call.m.localhost` via `pnpm dev --host `
- Element Web `app.m.localhost` and `app.othersite.m.localhost`
- Note certificates will expire on Thr, 20 September 2035 14:27:35 CEST
These use a test 'secret' published in this repository, so this must be used
only for local development and **_never be exposed to the public Internet._**
-Run backend components:
+Make sure your Docker runtime is running (e.g. via `colima start`) and then start
+the backend components:
```sh
pnpm backend
diff --git a/docs/controls.md b/docs/controls.md
index e5e0746d..b97fe795 100644
--- a/docs/controls.md
+++ b/docs/controls.md
@@ -12,7 +12,7 @@ A few aspects of Element Call's interface can be controlled through a global API
On mobile platforms (iOS, Android), web views do not reliably support selecting audio output devices such as the main speaker, earpiece, or headset. To address this limitation, the following functions allow the hosting application (e.g., Element Web, Element X) to manage audio devices via exposed JavaScript interfaces. These functions must be enabled using the URL parameter `controlledAudioDevices` to take effect.
-- `controls.setAvailableAudioDevices(devices: { id: string, name: string, forEarpiece?: boolean, isEarpiece?: boolean isSpeaker?: boolean, isExternalHeadset?, boolean; }[]): void` Sets the list of available audio outputs. `forEarpiece` is used on iOS only.
+- `controls.setAvailableAudioDevices(devices: { id: string, name: string, forEarpiece?: boolean, isEarpiece?: boolean isSpeaker?: boolean, isExternalHeadset?: boolean }[]): void` Sets the list of available audio outputs. `forEarpiece` is used on iOS only.
It flags the device that should be used if the user selects earpiece mode. This should be the main stereo loudspeaker of the device.
- `controls.onAudioDeviceSelect: ((id: string) => void) | undefined` Callback called whenever the user or application selects a new audio output.
- `controls.setAudioDevice(id: string): void` Sets the selected audio device in Element Call's menu. This should be used if the OS decides to automatically switch to Bluetooth, for example.
diff --git a/docs/embedded_standalone.md b/docs/embedded_standalone.md
index 24ad2a7d..456ce120 100644
--- a/docs/embedded_standalone.md
+++ b/docs/embedded_standalone.md
@@ -25,7 +25,7 @@ The basics are:
1. Add the appropriate platform dependency as given for a [release](https://github.com/element-hq/element-call/releases), or use the embedded tarball. e.g. `npm install @element-hq/element-call-embedded@0.9.0`
2. Include the assets from the platform dependency in the build process. e.g. copy the assets during a [Webpack](https://github.com/element-hq/element-web/blob/247cd8d56d832d006d7dfb919d1042529d712b59/webpack.config.js#L677-L682) build.
-3. Use the `index.html` entrypointof the imported assets when you are constructing the WebView or iframe. e.g. using a [relative path in a webapp](https://github.com/element-hq/element-web/blob/247cd8d56d832d006d7dfb919d1042529d712b59/src/models/Call.ts#L680), or on the the Android [WebViewAssetLoader](https://github.com/element-hq/element-x-android/blob/fe5aab6588ecdcf9354a3bfbd9e97c1b31175a8f/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt#L20)
+3. Use the `index.html` entrypoint of the imported assets when you are constructing the WebView or iframe. e.g. using a [relative path in a webapp](https://github.com/element-hq/element-web/blob/247cd8d56d832d006d7dfb919d1042529d712b59/src/models/Call.ts#L680), or on the the Android [WebViewAssetLoader](https://github.com/element-hq/element-x-android/blob/fe5aab6588ecdcf9354a3bfbd9e97c1b31175a8f/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt#L20)
4. Set any of the [embedded-only URL parameters](./url_params.md#embedded-only-parameters) that you need.
## Widget vs standalone mode
@@ -35,5 +35,5 @@ Element Call is developed using the [js-sdk](https://github.com/matrix-org/matri
As a widget, the app only uses the core calling (MatrixRTC) parts. The rest (authentication, sending events, getting room state updates about calls) is done by the hosting client.
Element Call and the hosting client are connected via the widget API.
-Element Call detects that it is run as a widget if a widgetId is defined in the url parameters. If `widgetId` is present then Element Call will try to connect to the client via the widget postMessage API using the parameters provided in [Url Format and parameters
+Element Call detects that it is run as a widget if `widgetId` is defined in the url parameters. If `widgetId` is present then Element Call will try to connect to the client via the widget postMessage API using the parameters provided in [Url Format and parameters
](./url_params.md).
diff --git a/docs/linking.md b/docs/linking.md
index 1016fffb..3a18844d 100644
--- a/docs/linking.md
+++ b/docs/linking.md
@@ -1,6 +1,6 @@
## Quickstart guide
-run
+Run:
```bash
./scripts/setup-linking.sh
@@ -50,7 +50,7 @@ before committing a change.
To make this less of a foot gun we added a git hook.
A `pre-commit` hook will check if linking is currently used. If it detects
a `.pnpmfile.cjs` file it will abort the commit with an explanatory message.
-You will than need to run `pnpm links:off` and commit again.
+You will then need to run `pnpm links:off` and commit again.
To activate the hooks configure git with (when using the setup script (`./scripts/setup-linking.sh`) this is already done):
diff --git a/docs/linking_concept_reasoning.md b/docs/linking_concept_reasoning.md
index 7c135a96..d065ba0b 100644
--- a/docs/linking_concept_reasoning.md
+++ b/docs/linking_concept_reasoning.md
@@ -11,7 +11,7 @@ When the renovate bot creates a PR it runs `pnpm install --ignore-pnpmfile`. Thi
This breaks builds that **don't** ignore the `.pnpmfile.cjs`-file. (CI that runs on the renovate PR)
From here we have two possible paths:
-- ignore `.pnpmfile.cjs` in all CI builds CI will also fail if we accidently add it locally.
+- ignore `.pnpmfile.cjs` in all CI builds (CI will also fail if we accidently add it locally).
- fixup the `pnpm-lock.yaml` in the renovate PR to contain the correct `pnpmfileChecksum`.
Ignoring in all CI builds means that CI will always fail if we enable the linking system.
@@ -22,9 +22,9 @@ Only if we remember setting it back/disbale linking (or let ourselves remember b
#### Summary
- We will always run into conflicts with the `pnpmfileChecksum` because in renovate prs it will be empty (`--ignore-pnpmfile`)
-- To keep it simple we set `--ignore-pnpmfile` in all of ours CI to see issues immediately.
+- To keep it simple we set `--ignore-pnpmfile` in all of our CI builds to see issues immediately.
- The only solution is to never have a `.pnpmfile.cjs` in the repository when pushing.
- This way there will never be a commit with `pnpmfileChecksum` in the lockfile.
- - renovate (which uses `--ignore-pnpmfile` which we cannot disable) and other CI will work
+ - renovate (which uses `--ignore-pnpmfile` which we cannot disable) and other CI will work.
- We are able to use the linking system locally if we `cp` this file from the scripts folder into `./` on demand.
- `pnpm links:on` and `pnpm links:off` + `./scripts/setup-linking.sh` will help us with this.
diff --git a/docs/self_hosting.md b/docs/self_hosting.md
index dc1dd687..e8ea2f6d 100644
--- a/docs/self_hosting.md
+++ b/docs/self_hosting.md
@@ -58,7 +58,7 @@ rc_message:
rc_delayed_event_mgmt:
# This needs to match at least the heart-beat frequency plus a bit of headroom
- # Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
+ # Currently the heart-beat is every 5 seconds which translates into a rate of 0.2Hz
per_second: 1
burst_count: 20
```
@@ -70,7 +70,7 @@ make sure that your Synapse server has either a `federation` or `openid`
### MatrixRTC Backend
-In order to **guarantee smooth operation** of Element Call MatrixRTC backend is
+In order to **guarantee smooth operation** of Element Call, a MatrixRTC backend is
required for each site deployment.

@@ -190,8 +190,8 @@ backend mxrtc_auth_backend
> [!IMPORTANT]
> As defined in
-> [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
-> MatrixRTC backend must be announced to the client via your **Matrix site's
+> [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143),
+> the MatrixRTC backend(s) must be announced to the client via your **Matrix site's
> `.well-known/matrix/client`** file (e.g.
> `example.com/.well-known/matrix/client` matching the site deployment example
> from above). The configuration is a list of Foci configs:
@@ -222,7 +222,7 @@ Access-Control-Allow-Headers: X-Requested-With, Content-Type, Authorization
> [!NOTE]
> Most `org.matrix.msc4143.rtc_foci` configurations will only have one entry in
-> the array
+> the array.
## Building Element Call
@@ -291,7 +291,7 @@ be able to handle those yet and it may behave unreliably.
Therefore, to use a self-hosted homeserver, this is recommended to be a new
server where any user account created has not joined any normal rooms anywhere
-in the Matrix federated network. The homeserver used can be setup to disable
+in the Matrix federated network. The homeserver used can be set up to disable
federation, so as to prevent spam registrations (if you keep registrations open)
and to ensure Element Call continues to work in case any user decides to log in
to their Element Call account using the standard Element app and joins normal
diff --git a/docs/url_params.md b/docs/url_params.md
index e88e7095..4d567e84 100644
--- a/docs/url_params.md
+++ b/docs/url_params.md
@@ -12,7 +12,7 @@ https://element_call.domain/room/#
```
The URL is split into two sections. The `https://element_call.domain/room/#`
-contains the app and the intend that the link brings you into a specific room
+contains the app and the intent that the link brings you into a specific room
(`https://call.element.io/#` would be the homepage). The fragment is used for
query parameters to make sure they never get sent to the element_call.domain
server. Here we have the actual Matrix room ID and the password which are used
@@ -76,16 +76,16 @@ These parameters are relevant to both [widget](./embedded_standalone.md) and [st
These parameters are only supported in [widget](./embedded_standalone.md) mode.
-| Name | Values | Required | Description |
-| --------------- | ----------------------------------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `baseUrl` | | Yes | The base URL of the homeserver to use for media lookups. |
-| `deviceId` | Matrix device ID | Yes | The Matrix device ID for the widget host. |
-| `parentUrl` | | Yes | The url used to send widget action postMessages. This should be the domain of the client or the webview the widget is hosted in. (in case the widget is not in an Iframe but in a dedicated webview we send the postMessages same WebView the widget lives in. Filtering is done in the widget so it ignores the messages it receives from itself) |
-| `posthogUserId` | Posthog user identifier | No | This replaces the `analyticsID` parameter |
-| `preload` | `true` or `false` | No, defaults to `false` | Pauses app before joining a call until an `io.element.join` widget action is seen, allowing preloading. |
-| `returnToLobby` | `true` or `false` | No, defaults to `false` | Displays the lobby in widget mode after leaving a call; shows a blank page if set to `false`. Useful for video rooms. |
-| `userId` | [Matrix User Identifier](https://spec.matrix.org/v1.12/appendices/#user-identifiers) | Yes | The Matrix user ID. |
-| `widgetId` | [MSC2774](https://github.com/matrix-org/matrix-spec-proposals/pull/2774) format widget ID | Yes | The id used by the widget. The presence of this parameter implies that element call will not connect to a homeserver directly and instead tries to establish postMessage communication via the `parentUrl`. |
+| Name | Values | Required | Description |
+| --------------- | ----------------------------------------------------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `baseUrl` | | Yes | The base URL of the homeserver to use for media lookups. |
+| `deviceId` | Matrix device ID | Yes | The Matrix device ID for the widget host. |
+| `parentUrl` | | Yes | The url used to send widget action postMessages. This should be the domain of the client or the webview the widget is hosted in. (In case the widget is not in an Iframe but in a dedicated webview, we send the postMessages in the same WebView the widget lives in. Filtering is done in the widget so it ignores the messages it receives from itself.) |
+| `posthogUserId` | Posthog user identifier | No | This replaces the `analyticsID` parameter |
+| `preload` | `true` or `false` | No, defaults to `false` | Pauses app before joining a call until an `io.element.join` widget action is seen, allowing preloading. |
+| `returnToLobby` | `true` or `false` | No, defaults to `false` | Displays the lobby in widget mode after leaving a call; shows a blank page if set to `false`. Useful for video rooms. |
+| `userId` | [Matrix User Identifier](https://spec.matrix.org/v1.12/appendices/#user-identifiers) | Yes | The Matrix user ID. |
+| `widgetId` | [MSC2774](https://github.com/matrix-org/matrix-spec-proposals/pull/2774) format widget ID | Yes | The id used by the widget. The presence of this parameter implies that element call will not connect to a homeserver directly and instead tries to establish postMessage communication via the `parentUrl`. |
### Embedded-only parameters
diff --git a/locales/en/app.json b/locales/en/app.json
index b51c6ed9..a14663e9 100644
--- a/locales/en/app.json
+++ b/locales/en/app.json
@@ -3,6 +3,7 @@
"user_menu": "User menu"
},
"action": {
+ "blur_background": "Blur background",
"close": "Close",
"copy_link": "Copy link",
"edit": "Edit",
diff --git a/package.json b/package.json
index fef415eb..1e3a6fee 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,8 @@
"i18n": "i18next",
"i18n:check": "i18next --fail-on-warnings --fail-on-update",
"test": "vitest",
+ "test:storybook": "vitest --project=storybook",
+ "test:unit": "vitest --project=unit",
"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",
@@ -39,7 +41,7 @@
},
"devDependencies": {
"@babel/core": "^7.16.5",
- "@babel/preset-env": "^7.22.20",
+ "@babel/preset-env": "^7.29.5",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.0",
"@codecov/vite-plugin": "^1.3.0",
@@ -52,15 +54,16 @@
"@livekit/protocol": "^1.42.2",
"@livekit/track-processors": "^0.7.1",
"@mediapipe/tasks-vision": "^0.10.18",
- "@playwright/test": "^1.57.0",
+ "@playwright/test": "^1.60.0",
"@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": "^10.0.0",
"@sentry/react": "^8.0.0",
"@sentry/vite-plugin": "^3.0.0",
- "@storybook/addon-docs": "^10.3.3",
- "@storybook/react-vite": "^10.3.3",
+ "@storybook/addon-docs": "^10.3.6",
+ "@storybook/addon-vitest": "^10.3.6",
+ "@storybook/react-vite": "^10.3.6",
"@stylistic/eslint-plugin": "^3.0.0",
"@testing-library/dom": "^10.1.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -83,7 +86,9 @@
"@vector-im/compound-design-tokens": "^10.0.0",
"@vector-im/compound-web": "^9.3.0",
"@vitejs/plugin-react": "^4.0.1",
+ "@vitest/browser-playwright": "^4.1.5",
"@vitest/coverage-v8": "^4.0.18",
+ "@vitest/ui": "4.1.5",
"babel-plugin-transform-vite-meta-env": "^1.0.3",
"classnames": "^2.3.1",
"copy-to-clipboard": "^3.3.3",
@@ -98,7 +103,7 @@
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-rxjs": "^5.0.3",
- "eslint-plugin-storybook": "^10.3.3",
+ "eslint-plugin-storybook": "^10.3.6",
"eslint-plugin-unicorn": "^56.0.0",
"fetch-mock": "11.1.5",
"global-jsdom": "^26.0.0",
@@ -118,7 +123,7 @@
"pako": "^2.0.4",
"postcss": "^8.4.41",
"postcss-preset-env": "^10.0.0",
- "posthog-js": "1.160.3",
+ "posthog-js": "1.374.0",
"prettier": "^3.0.0",
"qrcode": "^1.5.4",
"react": "19",
@@ -128,7 +133,7 @@
"react-use-measure": "^2.1.1",
"rxjs": "^7.8.1",
"sass": "^1.42.1",
- "storybook": "^10.3.3",
+ "storybook": "^10.3.6",
"typescript": "^5.8.3",
"typescript-eslint-language-service": "^5.0.5",
"unique-names-generator": "^4.6.0",
@@ -141,7 +146,7 @@
"vite-plugin-node-stdlib-browser": "^0.2.1",
"vite-plugin-svgr": "^4.0.0",
"vite-plugin-wasm": "^3.6.0",
- "vitest": "^4.0.18",
+ "vitest": "^4.1.5",
"vitest-axe": "^1.0.0-pre.3"
},
"pnpm": {
diff --git a/playwright/mobile/create-call-mobile.spec.ts b/playwright/mobile/create-call-mobile.spec.ts
index 1d9d3af0..f07793f7 100644
--- a/playwright/mobile/create-call-mobile.spec.ts
+++ b/playwright/mobile/create-call-mobile.spec.ts
@@ -77,13 +77,13 @@ mobileTest(
await expect(
guestPage.getByTestId("roomHeader_participants_count"),
).toContainText("2");
- expect(await guestPage.getByTestId("videoTile").count()).toBe(2);
+ await expect(guestPage.getByTestId("videoTile")).toHaveCount(2);
// Same in creator page
await expect(
creatorPage.getByTestId("roomHeader_participants_count"),
).toContainText("2");
- expect(await creatorPage.getByTestId("videoTile").count()).toBe(2);
+ await expect(creatorPage.getByTestId("videoTile")).toHaveCount(2);
// TEST: control audio devices from the invitee page
diff --git a/playwright/reconnect.spec.ts b/playwright/reconnect.spec.ts
index 1a8f2c28..bd4dd199 100644
--- a/playwright/reconnect.spec.ts
+++ b/playwright/reconnect.spec.ts
@@ -54,6 +54,8 @@ test("can only interact with header and footer while reconnecting", async ({
page.getByRole("switch", { name: "Mute microphone" }),
).toBeFocused();
await page.keyboard.press("Tab");
+ await expect(page.getByRole("button", { name: "Microphone" })).toBeFocused();
+ await page.keyboard.press("Tab");
await expect(page.getByRole("switch", { name: "Stop video" })).toBeFocused();
// Most critically, we should be able to press the hangup button
await page.getByRole("button", { name: "End call" }).click();
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 35fbc66f..6d1c4e0b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -22,8 +22,8 @@ importers:
specifier: ^7.16.5
version: 7.29.0
'@babel/preset-env':
- specifier: ^7.22.20
- version: 7.29.2(@babel/core@7.29.0)
+ specifier: ^7.29.5
+ version: 7.29.5(@babel/core@7.29.0)
'@babel/preset-react':
specifier: ^7.22.15
version: 7.28.5(@babel/core@7.29.0)
@@ -47,22 +47,22 @@ importers:
version: 11.7.12
'@livekit/components-core':
specifier: ^0.12.0
- version: 0.12.13(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
+ version: 0.12.13(livekit-client@2.18.10(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
'@livekit/components-react':
specifier: ^2.0.0
- version: 2.9.21(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)
+ version: 2.9.21(livekit-client@2.18.10(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)
'@livekit/protocol':
specifier: ^1.42.2
version: 1.45.6
'@livekit/track-processors':
specifier: ^0.7.1
- version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))
+ version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.10(@types/dom-mediacapture-record@1.0.22))
'@mediapipe/tasks-vision':
specifier: ^0.10.18
version: 0.10.34
'@playwright/test':
- specifier: ^1.57.0
- version: 1.59.1
+ specifier: ^1.60.0
+ version: 1.60.0
'@radix-ui/react-dialog':
specifier: ^1.0.4
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -82,11 +82,14 @@ importers:
specifier: ^3.0.0
version: 3.6.1
'@storybook/addon-docs':
- specifier: ^10.3.3
- version: 10.3.5(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ specifier: ^10.3.6
+ version: 10.3.6(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ '@storybook/addon-vitest':
+ specifier: ^10.3.6
+ version: 10.3.6(@vitest/browser-playwright@4.1.5)(@vitest/browser@4.1.5)(@vitest/runner@4.1.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vitest@4.1.5)
'@storybook/react-vite':
- specifier: ^10.3.3
- version: 10.3.5(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ specifier: ^10.3.6
+ version: 10.3.6(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
'@stylistic/eslint-plugin':
specifier: ^3.0.0
version: 3.1.0(eslint@8.57.1)(typescript@5.9.3)
@@ -153,8 +156,14 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.0.1
version: 4.7.0(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ '@vitest/browser-playwright':
+ specifier: ^4.1.5
+ version: 4.1.5(playwright@1.60.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
'@vitest/coverage-v8':
specifier: ^4.0.18
+ version: 4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5)
+ '@vitest/ui':
+ specifier: 4.1.5
version: 4.1.5(vitest@4.1.5)
babel-plugin-transform-vite-meta-env:
specifier: ^1.0.3
@@ -199,8 +208,8 @@ importers:
specifier: ^5.0.3
version: 5.0.3(eslint@8.57.1)(typescript@5.9.3)
eslint-plugin-storybook:
- specifier: ^10.3.3
- version: 10.3.5(eslint@8.57.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)
+ specifier: ^10.3.6
+ version: 10.3.6(eslint@8.57.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)
eslint-plugin-unicorn:
specifier: ^56.0.0
version: 56.0.1(eslint@8.57.1)
@@ -227,7 +236,7 @@ importers:
version: 5.88.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.12.2)(typescript@5.9.3)
livekit-client:
specifier: ^2.18.1
- version: 2.18.9(@types/dom-mediacapture-record@1.0.22)
+ version: 2.18.10(@types/dom-mediacapture-record@1.0.22)
lodash-es:
specifier: ^4.17.21
version: 4.18.1
@@ -259,8 +268,8 @@ importers:
specifier: ^10.0.0
version: 10.6.1(postcss@8.5.11)
posthog-js:
- specifier: 1.160.3
- version: 1.160.3
+ specifier: 1.374.0
+ version: 1.374.0
prettier:
specifier: ^3.0.0
version: 3.8.3
@@ -289,8 +298,8 @@ importers:
specifier: ^1.42.1
version: 1.99.0
storybook:
- specifier: ^10.3.3
- version: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ specifier: ^10.3.6
+ version: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
typescript:
specifier: ^5.8.3
version: 5.9.3
@@ -328,8 +337,8 @@ importers:
specifier: ^3.6.0
version: 3.6.0(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
vitest:
- specifier: ^4.0.18
- version: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ specifier: ^4.1.5
+ version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
vitest-axe:
specifier: ^1.0.0-pre.3
version: 1.0.0-pre.5(vitest@4.1.5)
@@ -361,8 +370,8 @@ packages:
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
- '@babel/compat-data@7.29.0':
- resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
+ '@babel/compat-data@7.29.3':
+ resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==}
engines: {node: '>=6.9.0'}
'@babel/core@7.29.0':
@@ -502,6 +511,12 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
+ '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3':
+ resolution: {integrity: sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
'@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1':
resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==}
engines: {node: '>=6.9.0'}
@@ -700,8 +715,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-modules-systemjs@7.29.0':
- resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==}
+ '@babel/plugin-transform-modules-systemjs@7.29.4':
+ resolution: {integrity: sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -898,8 +913,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
- '@babel/preset-env@7.29.2':
- resolution: {integrity: sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==}
+ '@babel/preset-env@7.29.5':
+ resolution: {integrity: sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -941,6 +956,9 @@ packages:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
+ '@blazediff/core@1.9.1':
+ resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==}
+
'@bufbuild/protobuf@1.10.1':
resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==}
@@ -1572,12 +1590,12 @@ packages:
'@livekit/mutex@1.1.1':
resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==}
- '@livekit/protocol@1.45.3':
- resolution: {integrity: sha512-WmMxBTsy4dRBqcrswFwUUlgq3Z0nnhOqKR6tX749Rb/PcB1yBMUtrHxZvcsS6qi3/5+86zHeVG+exmu1sZqfJg==}
-
'@livekit/protocol@1.45.6':
resolution: {integrity: sha512-YPDmrUiVe1EY/q/2bD+Fp+69DWq6LZgeH+G/KEbz07OIVf8hgAYzfb1FgiOdWLRpSj06+SuTmrOY604fWNuD3w==}
+ '@livekit/protocol@1.45.8':
+ resolution: {integrity: sha512-Q+l57E7w/xxOBFVWzdX5rkAZO7ffyF+rlDzNUYq2SU114+5aTyCq+PK4unaEVDNd4952Af7wteKr3sOgasGuaA==}
+
'@livekit/track-processors@0.7.2':
resolution: {integrity: sha512-lzARBKTbBwqycdR/SwTu6//N0l20BzfDd7grxCXl07676SwRApNtZAK1GJjL1m3dCM3KBqH1aVxjMpNcbOw5uQ==}
peerDependencies:
@@ -1666,6 +1684,78 @@ packages:
'@octokit/types@13.10.0':
resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==}
+ '@opentelemetry/api-logs@0.208.0':
+ resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==}
+ engines: {node: '>=8.0.0'}
+
+ '@opentelemetry/api@1.9.1':
+ resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
+ engines: {node: '>=8.0.0'}
+
+ '@opentelemetry/core@2.2.0':
+ resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
+ '@opentelemetry/core@2.7.1':
+ resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
+ '@opentelemetry/exporter-logs-otlp-http@0.208.0':
+ resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/otlp-exporter-base@0.208.0':
+ resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/otlp-transformer@0.208.0':
+ resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/resources@2.2.0':
+ resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.3.0 <1.10.0'
+
+ '@opentelemetry/resources@2.7.1':
+ resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.3.0 <1.10.0'
+
+ '@opentelemetry/sdk-logs@0.208.0':
+ resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.4.0 <1.10.0'
+
+ '@opentelemetry/sdk-metrics@2.2.0':
+ resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.9.0 <1.10.0'
+
+ '@opentelemetry/sdk-trace-base@2.2.0':
+ resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.3.0 <1.10.0'
+
+ '@opentelemetry/semantic-conventions@1.41.1':
+ resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==}
+ engines: {node: '>=14'}
+
'@oxc-project/types@0.127.0':
resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==}
@@ -1869,11 +1959,50 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
- '@playwright/test@1.59.1':
- resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==}
+ '@playwright/test@1.60.0':
+ resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==}
engines: {node: '>=18'}
hasBin: true
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
+ '@posthog/core@1.29.3':
+ resolution: {integrity: sha512-OvJSAzqVfZx+L7D874q56FVRTxOIsFBVB3wSB/Uny+DhmfNRGDi1rpZAruEmQYl9WQlQJb1q6JXGAC+rxVXjPA==}
+
+ '@posthog/types@1.374.0':
+ resolution: {integrity: sha512-qouREpHIxsBS3Gc6a5gZvg6/ykK+4TJAs4wYTUIH/emH1HQfaaLrWzGoEm+/OPwlNxHzw4tQn9OOyxsmr9NF2g==}
+
+ '@protobufjs/aspromise@1.1.2':
+ resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
+
+ '@protobufjs/base64@1.1.2':
+ resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
+
+ '@protobufjs/codegen@2.0.5':
+ resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==}
+
+ '@protobufjs/eventemitter@1.1.0':
+ resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
+
+ '@protobufjs/fetch@1.1.1':
+ resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==}
+
+ '@protobufjs/float@1.0.2':
+ resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
+
+ '@protobufjs/inquire@1.1.2':
+ resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==}
+
+ '@protobufjs/path@1.1.2':
+ resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
+
+ '@protobufjs/pool@1.1.0':
+ resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
+
+ '@protobufjs/utf8@1.1.1':
+ resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==}
+
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -2679,23 +2808,41 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
- '@storybook/addon-docs@10.3.5':
- resolution: {integrity: sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==}
+ '@storybook/addon-docs@10.3.6':
+ resolution: {integrity: sha512-TvIdADVPtauxW0LzXIpIv7X6GxwetorhyNh+6+7MHC27XSBCWVxxRUwL63YeLlHTuXsIk0quG3b1xgwVRzWOJA==}
peerDependencies:
- storybook: ^10.3.5
+ storybook: ^10.3.6
- '@storybook/builder-vite@10.3.5':
- resolution: {integrity: sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==}
+ '@storybook/addon-vitest@10.3.6':
+ resolution: {integrity: sha512-HXj7RrPJY+xzoNjL+xZu2oLw1fI5BA87Noh1NAXMPuECHR5R5fuRM/tTsJuIGXHFMO06FjSi/rekDIfCj1fL4w==}
peerDependencies:
- storybook: ^10.3.5
+ '@vitest/browser': ^3.0.0 || ^4.0.0
+ '@vitest/browser-playwright': ^4.0.0
+ '@vitest/runner': ^3.0.0 || ^4.0.0
+ storybook: ^10.3.6
+ vitest: ^3.0.0 || ^4.0.0
+ peerDependenciesMeta:
+ '@vitest/browser':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/runner':
+ optional: true
+ vitest:
+ optional: true
+
+ '@storybook/builder-vite@10.3.6':
+ resolution: {integrity: sha512-gpvR/sE4BcrFtmQZ+Ker7zD23oQzoVeqD9nF6cK6yzY+Q0svJXyX2EPmFG4y+EwygD5/vNzDpP84gGMut8VRwg==}
+ peerDependencies:
+ storybook: ^10.3.6
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
- '@storybook/csf-plugin@10.3.5':
- resolution: {integrity: sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==}
+ '@storybook/csf-plugin@10.3.6':
+ resolution: {integrity: sha512-9kBf7VRdRqTSIYo+rPtVn5yjYYyK8kP2QhEYx3oiXvfwy4RexmbJnhk/tXa/lNiTqukA1TqaWQ2+5MqF4fu6YQ==}
peerDependencies:
esbuild: ^0.28.0
rollup: '*'
- storybook: ^10.3.5
+ storybook: ^10.3.6
vite: '*'
webpack: '*'
peerDependenciesMeta:
@@ -2717,27 +2864,27 @@ packages:
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
- '@storybook/react-dom-shim@10.3.5':
- resolution: {integrity: sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==}
+ '@storybook/react-dom-shim@10.3.6':
+ resolution: {integrity: sha512-/Tu1gPu+Fw+zOnAGmxRmOD30FX3a04LxcTAKflEtdpmtIMVR5bA3qpjy+f5YhoyDCecbXyKmL1OeIU2FIIZHqQ==}
peerDependencies:
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
- storybook: ^10.3.5
+ storybook: ^10.3.6
- '@storybook/react-vite@10.3.5':
- resolution: {integrity: sha512-UB5sJHeh26bfd8sNMx2YPGYRYmErIdTRaLOT28m4bykQIa1l9IgVktsYg/geW7KsJU0lXd3oTbnUjLD+enpi3w==}
+ '@storybook/react-vite@10.3.6':
+ resolution: {integrity: sha512-tySQRc+8q7V2NkylQMNJjDV8zXy6tkxb8oDqw/DIhHhI9Xn77MTKVZ8Cihbo5NMm7HYTB6xDKr6wqdSMgdufYQ==}
peerDependencies:
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
- storybook: ^10.3.5
+ storybook: ^10.3.6
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
- '@storybook/react@10.3.5':
- resolution: {integrity: sha512-tpLTLaVGoA6fLK3ReyGzZUricq7lyPaV2hLPpj5wqdXLV/LpRtAHClUpNoPDYSBjlnSjL81hMZijbkGC3mA+gw==}
+ '@storybook/react@10.3.6':
+ resolution: {integrity: sha512-oZQZ6xayWe5IdHmFUTL0TL8rX/gpNNh9gWhT2vzW5eeUvlkVG/RBKdsja6Ndrk2s1D9vcnwiI6r6CNXy3IEEmg==}
peerDependencies:
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
- storybook: ^10.3.5
+ storybook: ^10.3.6
typescript: '>= 4.9.x'
peerDependenciesMeta:
typescript:
@@ -2953,6 +3100,9 @@ packages:
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
+ '@types/trusted-types@2.0.7':
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
'@types/uuid@10.0.0':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
@@ -3173,6 +3323,17 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+ '@vitest/browser-playwright@4.1.5':
+ resolution: {integrity: sha512-CWy0lBQJq97nionyJJdnaU4961IXTl43a7UCu5nHy51IoKxAt6PVIJLo+76rVl7KOOgcWHNkG4kbJu/pW7knvA==}
+ peerDependencies:
+ playwright: '*'
+ vitest: 4.1.5
+
+ '@vitest/browser@4.1.5':
+ resolution: {integrity: sha512-iCDGI8c4yg+xmjUg2VsygdAUSIIB4x5Rht/P68OXy1hPELKXHDkzh87lkuTcdYmemRChDkEpB426MmDjzC0ziA==}
+ peerDependencies:
+ vitest: 4.1.5
+
'@vitest/coverage-v8@4.1.5':
resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==}
peerDependencies:
@@ -3217,6 +3378,11 @@ packages:
'@vitest/spy@4.1.5':
resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==}
+ '@vitest/ui@4.1.5':
+ resolution: {integrity: sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==}
+ peerDependencies:
+ vitest: 4.1.5
+
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
@@ -3693,6 +3859,9 @@ packages:
core-js-compat@3.49.0:
resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==}
+ core-js@3.49.0:
+ resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
+
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@@ -3915,6 +4084,9 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
+ dompurify@3.4.5:
+ resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==}
+
domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
@@ -4165,11 +4337,11 @@ packages:
eslint: ^8.0.0
typescript: '>=4.0.0'
- eslint-plugin-storybook@10.3.5:
- resolution: {integrity: sha512-rEFkfU3ypF44GpB4tiJ9EFDItueoGvGi3+weLHZax2ON2MB7VIDsxdSUGvIU5tMURg+oWYlpzCyLm4TpDq2deA==}
+ eslint-plugin-storybook@10.3.6:
+ resolution: {integrity: sha512-8udrL+Rmp5LFaZvgRe4J226X1MYls25bWCyHuzR5X8s2qbFTryX+wKC+o/0Ato4A1AvwnDg8OOMPc6yWJ9JpcA==}
peerDependencies:
eslint: '>=8'
- storybook: ^10.3.5
+ storybook: ^10.3.6
eslint-plugin-unicorn@56.0.1:
resolution: {integrity: sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==}
@@ -4311,6 +4483,9 @@ packages:
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
+ fflate@0.8.2:
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -5023,8 +5198,8 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
- livekit-client@2.18.9:
- resolution: {integrity: sha512-l0cADcxxBCWCBMtU9eWY6RpdbRfgA5c1/05yngQXo08mcy3VOttmSE2pNZ74k2B2zQym149g5/Y1B3vq2FWwlw==}
+ livekit-client@2.18.10:
+ resolution: {integrity: sha512-0cmb1P/M78udlP3MP0xGz6KqgOqd9z/eH9ka+RMbFwISABfhSMpQWDEK39Tia4xnbKjk468+rVynjGkxokY1uA==}
peerDependencies:
'@types/dom-mediacapture-record': ^1
@@ -5053,6 +5228,9 @@ packages:
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
engines: {node: '>= 0.6.0'}
+ long@5.3.2:
+ resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
+
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -5154,6 +5332,10 @@ packages:
resolution: {integrity: sha512-Q9wJ/xhzeD9Wua1MwDN2v3ah3HENsUVSlzzL9Qw149cL9hHZkXtQGl3Eq36BbdLV+/qUwaP1WtJQ+H/+Oxso8g==}
engines: {node: 20 || 22 || 24}
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@@ -5411,13 +5593,13 @@ packages:
resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==}
engines: {node: '>=10'}
- playwright-core@1.59.1:
- resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==}
+ playwright-core@1.60.0:
+ resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==}
engines: {node: '>=18'}
hasBin: true
- playwright@1.59.1:
- resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==}
+ playwright@1.60.0:
+ resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==}
engines: {node: '>=18'}
hasBin: true
@@ -5429,6 +5611,10 @@ packages:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
+ pngjs@7.0.0:
+ resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
+ engines: {node: '>=14.19.0'}
+
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -5597,8 +5783,8 @@ packages:
resolution: {integrity: sha512-5dDj8+lmvA8XB78SmzGI8NlQoksv7IfutGWeVZxiixHbO+p4LDPT3wuG/D9sM/wrjZZ9I+Siy/e117vbFPxSZg==}
engines: {node: ^10 || ^12 || >=14}
- posthog-js@1.160.3:
- resolution: {integrity: sha512-mGvxOIlWPtdPx8EI0MQ81wNKlnH2K0n4RqwQOl044b34BCKiFVzZ7Hc7geMuZNaRAvCi5/5zyGeWHcAYZQxiMQ==}
+ posthog-js@1.374.0:
+ resolution: {integrity: sha512-3M2xsHXU7Hl64KGZjljq13jIKiJ4N7npY1n+1Q7VQmQKdVsoTc9geaeoHprZEZCMXp3b2qbWZEvIYjekUN5lAg==}
preact@10.29.1:
resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==}
@@ -5638,6 +5824,10 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ protobufjs@7.5.9:
+ resolution: {integrity: sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==}
+ engines: {node: '>=12.0.0'}
+
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -5660,6 +5850,9 @@ packages:
resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
engines: {node: '>=0.6'}
+ query-selector-shadow-dom@1.0.1:
+ resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==}
+
querystring-es3@0.2.1:
resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==}
engines: {node: '>=0.4.x'}
@@ -6059,6 +6252,10 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
+ sirv@3.0.2:
+ resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
+ engines: {node: '>=18'}
+
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@@ -6116,14 +6313,17 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
- storybook@10.3.5:
- resolution: {integrity: sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==}
+ storybook@10.3.6:
+ resolution: {integrity: sha512-vbSz7g/1rGMC1uAULqMZjALkIuLu2QABqfhRYhyr/11kzyesi+vAmwyJLukZP1FfecxGOgMwOh6GS0YsGpHAvQ==}
hasBin: true
peerDependencies:
prettier: ^2 || ^3
+ vite-plus: ^0.1.15
peerDependenciesMeta:
prettier:
optional: true
+ vite-plus:
+ optional: true
stream-browserify@3.0.0:
resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==}
@@ -6295,6 +6495,10 @@ packages:
toggle-selection@1.0.6:
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
tough-cookie@5.1.2:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
@@ -6683,8 +6887,8 @@ packages:
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
engines: {node: 20 || >=22}
- web-vitals@4.2.4:
- resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
+ web-vitals@5.2.0:
+ resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -6883,7 +7087,7 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
- '@babel/compat-data@7.29.0': {}
+ '@babel/compat-data@7.29.3': {}
'@babel/core@7.29.0':
dependencies:
@@ -6933,7 +7137,7 @@ snapshots:
'@babel/helper-compilation-targets@7.28.6':
dependencies:
- '@babel/compat-data': 7.29.0
+ '@babel/compat-data': 7.29.3
'@babel/helper-validator-option': 7.27.1
browserslist: 4.28.2
lru-cache: 5.1.1
@@ -7071,6 +7275,14 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
@@ -7286,7 +7498,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)':
+ '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
@@ -7501,9 +7713,9 @@ snapshots:
'@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
'@babel/helper-plugin-utils': 7.28.6
- '@babel/preset-env@7.29.2(@babel/core@7.29.0)':
+ '@babel/preset-env@7.29.5(@babel/core@7.29.0)':
dependencies:
- '@babel/compat-data': 7.29.0
+ '@babel/compat-data': 7.29.3
'@babel/core': 7.29.0
'@babel/helper-compilation-targets': 7.28.6
'@babel/helper-plugin-utils': 7.28.6
@@ -7511,6 +7723,7 @@ snapshots:
'@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0)
'@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.3(@babel/core@7.29.0)
'@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)
@@ -7542,7 +7755,7 @@ snapshots:
'@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
- '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.0)
'@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0)
'@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0)
@@ -7634,6 +7847,8 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
+ '@blazediff/core@1.9.1': {}
+
'@bufbuild/protobuf@1.10.1': {}
'@codecov/bundler-plugin-core@1.9.1':
@@ -8211,21 +8426,21 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
- '@livekit/components-core@0.12.13(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)':
+ '@livekit/components-core@0.12.13(livekit-client@2.18.10(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)':
dependencies:
'@floating-ui/dom': 1.7.4
- livekit-client: 2.18.9(@types/dom-mediacapture-record@1.0.22)
+ livekit-client: 2.18.10(@types/dom-mediacapture-record@1.0.22)
loglevel: 1.9.1
rxjs: 7.8.2
tslib: 2.8.1
- '@livekit/components-react@2.9.21(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)':
+ '@livekit/components-react@2.9.21(livekit-client@2.18.10(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)':
dependencies:
- '@livekit/components-core': 0.12.13(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
+ '@livekit/components-core': 0.12.13(livekit-client@2.18.10(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
clsx: 2.1.1
events: 3.3.0
jose: 6.2.3
- livekit-client: 2.18.9(@types/dom-mediacapture-record@1.0.22)
+ livekit-client: 2.18.10(@types/dom-mediacapture-record@1.0.22)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
tslib: 2.8.1
@@ -8233,19 +8448,19 @@ snapshots:
'@livekit/mutex@1.1.1': {}
- '@livekit/protocol@1.45.3':
- dependencies:
- '@bufbuild/protobuf': 1.10.1
-
'@livekit/protocol@1.45.6':
dependencies:
'@bufbuild/protobuf': 1.10.1
- '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))':
+ '@livekit/protocol@1.45.8':
+ dependencies:
+ '@bufbuild/protobuf': 1.10.1
+
+ '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.10(@types/dom-mediacapture-record@1.0.22))':
dependencies:
'@mediapipe/tasks-vision': 0.10.34
'@types/dom-mediacapture-transform': 0.1.11
- livekit-client: 2.18.9(@types/dom-mediacapture-record@1.0.22)
+ livekit-client: 2.18.10(@types/dom-mediacapture-record@1.0.22)
'@matrix-org/matrix-sdk-crypto-wasm@18.2.0': {}
@@ -8338,6 +8553,82 @@ snapshots:
dependencies:
'@octokit/openapi-types': 24.2.0
+ '@opentelemetry/api-logs@0.208.0':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+
+ '@opentelemetry/api@1.9.1': {}
+
+ '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/semantic-conventions': 1.41.1
+
+ '@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/semantic-conventions': 1.41.1
+
+ '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.208.0
+ '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.208.0
+ '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.1)
+ protobufjs: 7.5.9
+
+ '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.41.1
+
+ '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.41.1
+
+ '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.208.0
+ '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.41.1
+
+ '@opentelemetry/semantic-conventions@1.41.1': {}
+
'@oxc-project/types@0.127.0': {}
'@oxc-resolver/binding-android-arm-eabi@11.19.1':
@@ -8469,9 +8760,39 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
- '@playwright/test@1.59.1':
+ '@playwright/test@1.60.0':
dependencies:
- playwright: 1.59.1
+ playwright: 1.60.0
+
+ '@polka/url@1.0.0-next.29': {}
+
+ '@posthog/core@1.29.3':
+ dependencies:
+ '@posthog/types': 1.374.0
+
+ '@posthog/types@1.374.0': {}
+
+ '@protobufjs/aspromise@1.1.2': {}
+
+ '@protobufjs/base64@1.1.2': {}
+
+ '@protobufjs/codegen@2.0.5': {}
+
+ '@protobufjs/eventemitter@1.1.0': {}
+
+ '@protobufjs/fetch@1.1.1':
+ dependencies:
+ '@protobufjs/aspromise': 1.1.2
+
+ '@protobufjs/float@1.0.2': {}
+
+ '@protobufjs/inquire@1.1.2': {}
+
+ '@protobufjs/path@1.1.2': {}
+
+ '@protobufjs/pool@1.1.0': {}
+
+ '@protobufjs/utf8@1.1.1': {}
'@radix-ui/number@1.1.1': {}
@@ -9139,15 +9460,15 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
- '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
+ '@storybook/addon-docs@10.3.6(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
dependencies:
'@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5)
- '@storybook/csf-plugin': 10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ '@storybook/csf-plugin': 10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
'@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@storybook/react-dom-shim': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
ts-dedent: 2.2.0
transitivePeerDependencies:
- '@types/react'
@@ -9156,10 +9477,24 @@ snapshots:
- vite
- webpack
- '@storybook/builder-vite@10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
+ '@storybook/addon-vitest@10.3.6(@vitest/browser-playwright@4.1.5)(@vitest/browser@4.1.5)(@vitest/runner@4.1.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vitest@4.1.5)':
dependencies:
- '@storybook/csf-plugin': 10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
- storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@storybook/global': 5.0.0
+ '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ optionalDependencies:
+ '@vitest/browser': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
+ '@vitest/browser-playwright': 4.1.5(playwright@1.60.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
+ '@vitest/runner': 4.1.5
+ vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ transitivePeerDependencies:
+ - react
+ - react-dom
+
+ '@storybook/builder-vite@10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
+ dependencies:
+ '@storybook/csf-plugin': 10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
ts-dedent: 2.2.0
vite: 8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)
transitivePeerDependencies:
@@ -9167,9 +9502,9 @@ snapshots:
- rollup
- webpack
- '@storybook/csf-plugin@10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
+ '@storybook/csf-plugin@10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
dependencies:
- storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
unplugin: 2.3.11
optionalDependencies:
esbuild: 0.28.0
@@ -9183,25 +9518,25 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- '@storybook/react-dom-shim@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
+ '@storybook/react-dom-shim@10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
dependencies:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@storybook/react-vite@10.3.5(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
+ '@storybook/react-vite@10.3.6(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
- '@storybook/builder-vite': 10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
- '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)
+ '@storybook/builder-vite': 10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ '@storybook/react': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)
empathic: 2.0.0
magic-string: 0.30.21
react: 19.2.5
react-docgen: 8.0.3
react-dom: 19.2.5(react@19.2.5)
resolve: 1.22.12
- storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
tsconfig-paths: 4.2.0
vite: 8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)
transitivePeerDependencies:
@@ -9211,15 +9546,15 @@ snapshots:
- typescript
- webpack
- '@storybook/react@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)':
+ '@storybook/react@10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)':
dependencies:
'@storybook/global': 5.0.0
- '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
+ '@storybook/react-dom-shim': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
react: 19.2.5
react-docgen: 8.0.3
react-docgen-typescript: 2.4.0(typescript@5.9.3)
react-dom: 19.2.5(react@19.2.5)
- storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
@@ -9446,6 +9781,9 @@ snapshots:
'@types/tough-cookie@4.0.5': {}
+ '@types/trusted-types@2.0.7':
+ optional: true
+
'@types/uuid@10.0.0': {}
'@types/yargs-parser@21.0.3': {}
@@ -9745,7 +10083,37 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/coverage-v8@4.1.5(vitest@4.1.5)':
+ '@vitest/browser-playwright@4.1.5(playwright@1.60.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)':
+ dependencies:
+ '@vitest/browser': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
+ '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ playwright: 1.60.0
+ tinyrainbow: 3.1.0
+ vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ transitivePeerDependencies:
+ - bufferutil
+ - msw
+ - utf-8-validate
+ - vite
+
+ '@vitest/browser@4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)':
+ dependencies:
+ '@blazediff/core': 1.9.1
+ '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ '@vitest/utils': 4.1.5
+ magic-string: 0.30.21
+ pngjs: 7.0.0
+ sirv: 3.0.2
+ tinyrainbow: 3.1.0
+ vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ ws: 8.20.0
+ transitivePeerDependencies:
+ - bufferutil
+ - msw
+ - utf-8-validate
+ - vite
+
+ '@vitest/coverage-v8@4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.5
@@ -9757,7 +10125,9 @@ snapshots:
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
- vitest: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ optionalDependencies:
+ '@vitest/browser': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
'@vitest/expect@3.2.4':
dependencies:
@@ -9810,6 +10180,17 @@ snapshots:
'@vitest/spy@4.1.5': {}
+ '@vitest/ui@4.1.5(vitest@4.1.5)':
+ dependencies:
+ '@vitest/utils': 4.1.5
+ fflate: 0.8.2
+ flatted: 3.4.2
+ pathe: 2.0.3
+ sirv: 3.0.2
+ tinyglobby: 0.2.16
+ tinyrainbow: 3.1.0
+ vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
@@ -10000,7 +10381,7 @@ snapshots:
babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0):
dependencies:
- '@babel/compat-data': 7.29.0
+ '@babel/compat-data': 7.29.3
'@babel/core': 7.29.0
'@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0)
semver: 6.3.1
@@ -10343,6 +10724,8 @@ snapshots:
dependencies:
browserslist: 4.28.2
+ core-js@3.49.0: {}
+
core-util-is@1.0.3: {}
cosmiconfig@8.3.6(typescript@5.9.3):
@@ -10575,6 +10958,10 @@ snapshots:
dependencies:
domelementtype: 2.3.0
+ dompurify@3.4.5:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
domutils@2.8.0:
dependencies:
dom-serializer: 1.4.1
@@ -10966,11 +11353,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-storybook@10.3.5(eslint@8.57.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3):
+ eslint-plugin-storybook@10.3.6(eslint@8.57.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3):
dependencies:
- '@typescript-eslint/utils': 8.58.2(eslint@8.57.1)(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.59.3(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
- storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
transitivePeerDependencies:
- supports-color
- typescript
@@ -11151,6 +11538,8 @@ snapshots:
fflate@0.4.8: {}
+ fflate@0.8.2: {}
+
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@@ -11914,10 +12303,10 @@ snapshots:
lines-and-columns@1.2.4: {}
- livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22):
+ livekit-client@2.18.10(@types/dom-mediacapture-record@1.0.22):
dependencies:
'@livekit/mutex': 1.1.1
- '@livekit/protocol': 1.45.3
+ '@livekit/protocol': 1.45.8
'@types/dom-mediacapture-record': 1.0.22
events: 3.3.0
jose: 6.2.3
@@ -11945,6 +12334,8 @@ snapshots:
loglevel@1.9.2: {}
+ long@5.3.2: {}
+
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
@@ -12051,6 +12442,8 @@ snapshots:
mktemp@2.0.2: {}
+ mrmime@2.0.1: {}
+
ms@2.0.0: {}
ms@2.1.3: {}
@@ -12369,11 +12762,11 @@ snapshots:
dependencies:
find-up: 5.0.0
- playwright-core@1.59.1: {}
+ playwright-core@1.60.0: {}
- playwright@1.59.1:
+ playwright@1.60.0:
dependencies:
- playwright-core: 1.59.1
+ playwright-core: 1.60.0
optionalDependencies:
fsevents: 2.3.2
@@ -12381,6 +12774,8 @@ snapshots:
pngjs@5.0.0: {}
+ pngjs@7.0.0: {}
+
possible-typed-array-names@1.1.0: {}
postcss-attribute-case-insensitive@7.0.1(postcss@8.5.11):
@@ -12616,11 +13011,21 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
- posthog-js@1.160.3:
+ posthog-js@1.374.0:
dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.208.0
+ '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.1)
+ '@posthog/core': 1.29.3
+ '@posthog/types': 1.374.0
+ core-js: 3.49.0
+ dompurify: 3.4.5
fflate: 0.4.8
preact: 10.29.1
- web-vitals: 4.2.4
+ query-selector-shadow-dom: 1.0.1
+ web-vitals: 5.2.0
preact@10.29.1: {}
@@ -12653,6 +13058,21 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
+ protobufjs@7.5.9:
+ dependencies:
+ '@protobufjs/aspromise': 1.1.2
+ '@protobufjs/base64': 1.1.2
+ '@protobufjs/codegen': 2.0.5
+ '@protobufjs/eventemitter': 1.1.0
+ '@protobufjs/fetch': 1.1.1
+ '@protobufjs/float': 1.0.2
+ '@protobufjs/inquire': 1.1.2
+ '@protobufjs/path': 1.1.2
+ '@protobufjs/pool': 1.1.0
+ '@protobufjs/utf8': 1.1.1
+ '@types/node': 24.12.2
+ long: 5.3.2
+
proxy-from-env@1.1.0: {}
public-encrypt@4.0.3:
@@ -12678,6 +13098,8 @@ snapshots:
dependencies:
side-channel: 1.1.0
+ query-selector-shadow-dom@1.0.1: {}
+
querystring-es3@0.2.1: {}
queue-microtask@1.2.3: {}
@@ -13141,6 +13563,12 @@ snapshots:
signal-exit@4.1.0: {}
+ sirv@3.0.2:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
sisteransi@1.0.5: {}
slash@3.0.0: {}
@@ -13195,7 +13623,7 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
- storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@storybook/global': 5.0.0
'@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -13429,6 +13857,8 @@ snapshots:
toggle-selection@1.0.6: {}
+ totalist@3.0.1: {}
+
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.86
@@ -13788,9 +14218,9 @@ snapshots:
axe-core: 4.11.3
chalk: 5.6.2
lodash-es: 4.18.1
- vitest: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
+ vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
- vitest@4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)):
+ vitest@4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)):
dependencies:
'@vitest/expect': 4.1.5
'@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
@@ -13813,8 +14243,11 @@ snapshots:
vite: 8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)
why-is-node-running: 2.3.0
optionalDependencies:
+ '@opentelemetry/api': 1.9.1
'@types/node': 24.12.2
- '@vitest/coverage-v8': 4.1.5(vitest@4.1.5)
+ '@vitest/browser-playwright': 4.1.5(playwright@1.60.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
+ '@vitest/coverage-v8': 4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5)
+ '@vitest/ui': 4.1.5(vitest@4.1.5)
jsdom: 26.1.0
transitivePeerDependencies:
- msw
@@ -13836,7 +14269,7 @@ snapshots:
walk-up-path@4.0.0: {}
- web-vitals@4.2.4: {}
+ web-vitals@5.2.0: {}
webidl-conversions@3.0.1: {}
diff --git a/renovate.json b/renovate.json
index 39fbf0c1..18793689 100644
--- a/renovate.json
+++ b/renovate.json
@@ -49,6 +49,11 @@
"matchDepNames": ["vaul"],
"prHeader": "Please review modals on mobile for visual regressions."
},
+ {
+ "groupName": "PostHog",
+ "matchDepNames": ["posthog-js"],
+ "prHeader": "Please ensure that all analytics data is still appropriately sanitized."
+ },
{
"groupName": "embedded package dependencies",
"matchFileNames": ["embedded/**/*"]
@@ -59,7 +64,7 @@
}
],
"semanticCommits": "disabled",
- "ignoreDeps": ["posthog-js", "eslint-plugin-matrix-org"],
+ "ignoreDeps": ["eslint-plugin-matrix-org"],
"vulnerabilityAlerts": {
"schedule": ["at any time"],
"prHourlyLimit": 0,
diff --git a/src/AppBar.module.css b/src/AppBar.module.css
index 5f7888d2..13e3b759 100644
--- a/src/AppBar.module.css
+++ b/src/AppBar.module.css
@@ -1,5 +1,21 @@
.bar {
flex-shrink: 0;
+ position: relative;
+}
+
+/* Pseudo-element for the gradient background */
+.bar::before {
+ content: "";
+ position: absolute;
+ inset-inline: 0;
+ /* Extend the gradient beyond the bottom of the header for readability */
+ inset-block: -24px;
+ z-index: var(--call-view-header-footer-layer);
+ background: linear-gradient(
+ 0deg,
+ rgba(0, 0, 0, 0) 0%,
+ var(--cpd-color-bg-canvas-default) 100%
+ );
}
.bar > header {
diff --git a/src/analytics/PosthogAnalytics.test.ts b/src/analytics/PosthogAnalytics.test.ts
index 49af5eae..7c1128ad 100644
--- a/src/analytics/PosthogAnalytics.test.ts
+++ b/src/analytics/PosthogAnalytics.test.ts
@@ -14,8 +14,13 @@ import {
beforeAll,
afterAll,
} from "vitest";
+import posthog, { type CaptureResult } from "posthog-js";
-import { PosthogAnalytics } from "./PosthogAnalytics";
+import {
+ Anonymity,
+ santizeSensitiveData,
+ PosthogAnalytics,
+} from "./PosthogAnalytics";
import { mockConfig } from "../utils/test";
describe("PosthogAnalytics", () => {
@@ -88,4 +93,154 @@ describe("PosthogAnalytics", () => {
expect(PosthogAnalytics.instance.isEnabled()).toBe(true);
});
});
+
+ describe("applyPrivacyFilters", () => {
+ const makeEvent = (properties: Record
+
{
"region": "local",
"version": "1.2.3"
@@ -384,9 +418,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
Local Participant
-
+
localParticipantIdentity
@@ -413,9 +445,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
remote
)
-
+
{
"region": "remote",
"version": "4.5.6"
@@ -425,9 +455,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
Local Participant
-
+
localParticipantIdentity
diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts
index c288f73e..3cc6c2b4 100644
--- a/src/settings/rageshake.ts
+++ b/src/settings/rageshake.ts
@@ -502,6 +502,13 @@ export async function init(): Promise {
};
});
+ window.addEventListener("unhandledrejection", (event) => {
+ global.mx_rage_logger.log(
+ LogLevel.error,
+ `Unhandled promise rejection: ${event.reason}`,
+ );
+ });
+
return tryInitStorage();
}
diff --git a/src/settings/settings.ts b/src/settings/settings.ts
index 917c79f1..cf0d9d66 100644
--- a/src/settings/settings.ts
+++ b/src/settings/settings.ts
@@ -129,6 +129,11 @@ export const alwaysShowIphoneEarpiece = new Setting(
false,
);
+export const enableExtendedLivekitLogs = new Setting(
+ "extended-livekit-logs",
+ false,
+);
+
export enum MatrixRTCMode {
Legacy = "legacy",
Compatibility = "compatibility",
diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts
index cb528f68..fc25df48 100644
--- a/src/state/CallViewModel/CallViewModel.test.ts
+++ b/src/state/CallViewModel/CallViewModel.test.ts
@@ -50,6 +50,7 @@ import {
aliceParticipant,
aliceRtcMember,
aliceUserId,
+ bob,
bobId,
bobRtcMember,
local,
@@ -84,6 +85,14 @@ vi.mock("../e2ee/matrixKeyProvider");
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
vi.mock("../UrlParams", () => ({ getUrlParams }));
+const getPlatform = vi.hoisted(() => vi.fn(() => "desktop"));
+vi.mock("../../Platform", () => ({
+ get platform(): string {
+ return getPlatform();
+ },
+ isFirefox: (): boolean => false,
+}));
+
vi.mock(
"../state/CallViewModel/localMember/localTransport",
async (importOriginal) => ({
@@ -133,12 +142,19 @@ export interface SpotlightExpandedLayoutSummary {
pip?: string;
}
-export interface OneOnOneLayoutSummary {
- type: "one-on-one";
+export interface OneOnOneLandscapeLayoutSummary {
+ type: "one-on-one-landscape";
spotlight: string;
pip: string;
}
+export interface OneOnOnePortraitLayoutSummary {
+ type: "one-on-one-portrait";
+ spotlight: string[];
+ pip?: string;
+ pipSize: "sm" | "lg";
+}
+
export interface PipLayoutSummary {
type: "pip";
spotlight: string[];
@@ -149,7 +165,8 @@ export type LayoutSummary =
| SpotlightLandscapeLayoutSummary
| SpotlightPortraitLayoutSummary
| SpotlightExpandedLayoutSummary
- | OneOnOneLayoutSummary
+ | OneOnOneLandscapeLayoutSummary
+ | OneOnOnePortraitLayoutSummary
| PipLayoutSummary;
function summarizeLayout$(l$: Observable): Observable {
@@ -187,7 +204,7 @@ function summarizeLayout$(l$: Observable): Observable {
pip: pip?.id,
}),
);
- case "one-on-one":
+ case "one-on-one-landscape":
return combineLatest(
[l.spotlight.media$, l.pip.media$],
(spotlight, pip) => ({
@@ -196,6 +213,20 @@ function summarizeLayout$(l$: Observable): Observable {
pip: pip.id,
}),
);
+ case "one-on-one-portrait":
+ return combineLatest(
+ [
+ l.spotlight.media$,
+ l.pip?.media$ ?? constant(undefined),
+ l.pipSize$,
+ ],
+ (spotlight, pip, pipSize) => ({
+ type: l.type,
+ spotlight: spotlight.map((vm) => vm.id),
+ pip: pip?.id,
+ pipSize,
+ }),
+ );
case "pip":
return l.spotlight.media$.pipe(
map((spotlight) => ({
@@ -405,7 +436,7 @@ describe.each([
expectedLayoutMarbles,
{
a: {
- type: "one-on-one",
+ type: "one-on-one-landscape",
pip: `${localId}:0`,
spotlight: `${aliceId}:0`,
},
@@ -421,6 +452,85 @@ describe.each([
});
});
+ test("one-on-one portrait layout shows local tile when video is enabled", () => {
+ withTestScheduler(({ behavior, schedule, expectObservable }) => {
+ // Local participant enables their video, then disables it
+ const videoInputMarbles = " ny--n";
+ // While tile is shown, tap the screen twice
+ const tapScreenInputMarbles = "--aa-";
+ // Layout should show local tile, make it small, enlarge it again, then hide it
+ const expectedLayoutMarbles = "abcba";
+
+ withCallViewModel(
+ {
+ remoteParticipants$: constant([aliceParticipant]),
+ roomMembers: [local, alice],
+ rtcMembers$: constant([localRtcMember, aliceRtcMember]),
+ videoEnabled: new Map([
+ [localParticipant, behavior(videoInputMarbles, yesNo)],
+ ]),
+ windowSize$: constant({ width: 380, height: 700 }), // Mobile phone in portrait
+ },
+ (vm) => {
+ schedule(tapScreenInputMarbles, { a: () => vm.tapScreen() });
+
+ expectObservable(vm.edgeToEdge$).toBe("y", yesNo); // Edge-to-edge-layout
+ expectObservable(summarizeLayout$(vm.layout$)).toBe(
+ expectedLayoutMarbles,
+ {
+ a: {
+ type: "one-on-one-portrait",
+ spotlight: [`${aliceId}:0`],
+ pip: undefined,
+ pipSize: "lg",
+ },
+ b: {
+ type: "one-on-one-portrait",
+ spotlight: [`${aliceId}:0`],
+ pip: `${localId}:0`,
+ pipSize: "lg",
+ },
+ c: {
+ type: "one-on-one-portrait",
+ spotlight: [`${aliceId}:0`],
+ pip: `${localId}:0`,
+ pipSize: "sm",
+ },
+ },
+ );
+ },
+ );
+ });
+ });
+
+ test("one-on-one portrait layout shows name tags in room with 3 members", () => {
+ withTestScheduler(({ behavior, schedule, expectObservable }) => {
+ withCallViewModel(
+ {
+ remoteParticipants$: constant([aliceParticipant]),
+ // Both Alice and Bob are with us in the room
+ roomMembers: [local, alice, bob],
+ rtcMembers$: constant([localRtcMember, aliceRtcMember]),
+ windowSize$: constant({ width: 380, height: 700 }), // Mobile phone in portrait
+ },
+ (vm) => {
+ // Uses one-on-one portrait layout
+ expectObservable(summarizeLayout$(vm.layout$)).toBe("a", {
+ a: {
+ type: "one-on-one-portrait",
+ spotlight: [`${aliceId}:0`],
+ pip: undefined,
+ pipSize: "lg",
+ },
+ });
+ // It wouldn't be clear whether Alice or Bob is the remote video tile,
+ // so the interface must put a name tag on it
+ expectObservable(vm.showNameTags$).toBe("y", yesNo);
+ },
+ );
+ });
+ });
+
test("participants stay in the same order unless to appear/disappear", () => {
withTestScheduler(({ behavior, schedule, expectObservable }) => {
const visibilityInputMarbles = "a";
@@ -576,7 +686,7 @@ describe.each([
});
test("layout reacts to window size", () => {
- withTestScheduler(({ behavior, schedule, expectObservable }) => {
+ withTestScheduler(({ behavior, expectObservable }) => {
const windowSizeInputMarbles = "abc";
const expectedLayoutMarbles = " abc";
withCallViewModel(
@@ -584,7 +694,7 @@ describe.each([
remoteParticipants$: constant([aliceParticipant]),
rtcMembers$: constant([localRtcMember, aliceRtcMember]),
windowSize$: behavior(windowSizeInputMarbles, {
- a: { width: 300, height: 600 }, // Start very narrow, like a phone
+ a: { width: 380, height: 700 }, // Start very narrow, like a phone
b: { width: 1000, height: 800 }, // Go to normal desktop window size
c: { width: 200, height: 180 }, // Go to PiP size
}),
@@ -595,13 +705,14 @@ describe.each([
{
a: {
// This is the expected one-on-one layout for a narrow window
- type: "spotlight-expanded",
+ type: "one-on-one-portrait",
spotlight: [`${aliceId}:0`],
- pip: `${localId}:0`,
+ pip: undefined,
+ pipSize: "lg",
},
b: {
// In a larger window, expect the normal one-on-one layout
- type: "one-on-one",
+ type: "one-on-one-landscape",
pip: `${localId}:0`,
spotlight: `${aliceId}:0`,
},
@@ -735,6 +846,59 @@ describe.each([
});
});
+ // Test cases for footer visibility in PIP mode across different platforms
+ const PIP_FOOTER_VISIBILITY_TEST_CASES: Array<{
+ platform: "ios" | "android" | "desktop";
+ expectedMarbles: string;
+ description: string;
+ }> = [
+ {
+ platform: "ios",
+ expectedMarbles: "tf",
+ description: "hidden on iOS",
+ },
+ {
+ platform: "android",
+ expectedMarbles: "tf",
+ description: "hidden on Android",
+ },
+ {
+ platform: "desktop",
+ expectedMarbles: "t",
+ description: "visible on desktop",
+ },
+ ];
+
+ it.each(PIP_FOOTER_VISIBILITY_TEST_CASES)(
+ "footer is $description in PIP mode",
+ ({ platform: testPlatform, expectedMarbles }) => {
+ withTestScheduler(({ schedule, expectObservable }) => {
+ // Set platform for this test case
+ getPlatform.mockReturnValue(testPlatform);
+
+ // Enable PIP mode after initial render
+ const pipControlInputMarbles = "-e";
+
+ withCallViewModel(
+ {
+ remoteParticipants$: constant([aliceParticipant]),
+ rtcMembers$: constant([localRtcMember, aliceRtcMember]),
+ },
+ (vm) => {
+ schedule(pipControlInputMarbles, {
+ e: () => window.controls.enablePip(),
+ });
+
+ expectObservable(vm.showFooter$).toBe(expectedMarbles, {
+ t: true,
+ f: false,
+ });
+ },
+ );
+ });
+ },
+ );
+
test("PiP tile in expanded spotlight layout switches speakers without layout shifts", () => {
withTestScheduler(({ behavior, schedule, expectObservable }) => {
// Switch to spotlight immediately
@@ -956,7 +1120,7 @@ describe.each([
grid: [`${localId}:0`],
},
b: {
- type: "one-on-one",
+ type: "one-on-one-landscape",
pip: `${localId}:0`,
spotlight: `${aliceId}:0`,
},
@@ -999,7 +1163,7 @@ describe.each([
grid: [`${localId}:0`],
},
b: {
- type: "one-on-one",
+ type: "one-on-one-landscape",
pip: `${localId}:0`,
spotlight: `${aliceId}:0`,
},
@@ -1009,7 +1173,7 @@ describe.each([
grid: [`${localId}:0`, `${aliceId}:0`, `${daveId}:0`],
},
d: {
- type: "one-on-one",
+ type: "one-on-one-landscape",
pip: `${localId}:0`,
spotlight: `${daveId}:0`,
},
@@ -1227,7 +1391,7 @@ describe.each([
// ringing the entire time (even once timed out)
expectObservable(summarizeLayout$(vm.layout$)).toBe("a", {
a: {
- type: "one-on-one",
+ type: "one-on-one-landscape",
spotlight: `${localId}:0`,
pip: `ringing:${aliceUserId}`,
},
@@ -1266,12 +1430,12 @@ describe.each([
// ringing the entire time
expectObservable(summarizeLayout$(vm.layout$)).toBe("a 20ms b", {
a: {
- type: "one-on-one",
+ type: "one-on-one-landscape",
spotlight: `${localId}:0`,
pip: `ringing:${aliceUserId}`,
},
b: {
- type: "one-on-one",
+ type: "one-on-one-landscape",
spotlight: `${aliceId}:0`,
pip: `${localId}:0`,
},
diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts
index 07898b9e..2bbf6f4e 100644
--- a/src/state/CallViewModel/CallViewModel.ts
+++ b/src/state/CallViewModel/CallViewModel.ts
@@ -15,6 +15,7 @@ import {
} from "livekit-client";
import { type Room as MatrixRoom } from "matrix-js-sdk";
import {
+ BehaviorSubject,
catchError,
combineLatest,
distinctUntilChanged,
@@ -68,7 +69,8 @@ import { setPipEnabled$ } from "../../controls";
import { TileStore } from "../TileStore";
import { gridLikeLayout } from "../GridLikeLayout";
import { spotlightExpandedLayout } from "../SpotlightExpandedLayout";
-import { oneOnOneLayout } from "../OneOnOneLayout";
+import { oneOnOneLandscapeLayout } from "../OneOnOneLandscapeLayout";
+import { oneOnOnePortraitLayout } from "../OneOnOnePortraitLayout";
import { pipLayout } from "../PipLayout";
import { type EncryptionSystem } from "../../e2ee/sharedKeyManagement";
import {
@@ -86,10 +88,12 @@ import { getUrlParams, HeaderStyle } from "../../UrlParams";
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
import { ElementWidgetActions, widget } from "../../widget";
import {
+ type Alignment,
type GridLayoutMedia,
type Layout,
type LayoutMedia,
- type OneOnOneLayoutMedia,
+ type OneOnOneLandscapeLayoutMedia,
+ type OneOnOnePortraitLayoutMedia,
type SpotlightExpandedLayoutMedia,
type SpotlightLandscapeLayoutMedia,
type SpotlightPortraitLayoutMedia,
@@ -327,16 +331,6 @@ export interface CallViewModel {
{ sender: string; emoji: string; startX: number }[]
>;
- // window/layout
- /**
- * The general shape of the window.
- */
- windowMode$: Behavior;
- spotlightExpanded$: Behavior;
- toggleSpotlightExpanded$: Behavior<(() => void) | null>;
- gridMode$: Behavior;
- setGridMode: (value: GridMode) => void;
-
/**
* The layout of tiles in the call interface.
*/
@@ -347,10 +341,23 @@ export interface CallViewModel {
tileStoreGeneration$: Behavior;
showSpotlightIndicators$: Behavior;
showSpeakingIndicators$: Behavior;
+ showNameTags$: Behavior;
+ spotlightExpanded$: Behavior;
+ toggleSpotlightExpanded$: Behavior<(() => void) | null>;
+ gridMode$: Behavior;
+ setGridMode: (value: GridMode) => void;
// header/footer visibility
showHeader$: Behavior;
showFooter$: Behavior;
+ /**
+ * Whether the call layout should be displayed edge-to-edge, with the footer
+ * and header as overlays.
+ */
+ edgeToEdge$: Behavior;
+
+ settingsOpen$: Behavior;
+ setSettingsOpen$: Behavior<(open: boolean) => void>;
// audio routing
/**
@@ -777,6 +784,7 @@ export function createCallViewModel$(
callPickupState === "timeout" ||
callPickupState === "decline"
) {
+ // TODO: Respect io.element.functional_members
for (const member of roomMembers.values()) {
if (!userMedia.some((vm) => vm.userId === member.userId))
yield {
@@ -1057,6 +1065,7 @@ export function createCallViewModel$(
[grid$, spotlight$],
(grid, spotlight) => ({
type: "grid",
+ edgeToEdge: false,
spotlight: spotlight.some((vm) => vm.type === "screen share")
? spotlight
: undefined,
@@ -1064,9 +1073,12 @@ export function createCallViewModel$(
}),
);
- const spotlightLandscapeLayoutMedia$: Observable =
+ const spotlightLandscapeLayoutMedia$ = (
+ edgeToEdge: boolean,
+ ): Observable =>
combineLatest([grid$, spotlight$], (grid, spotlight) => ({
type: "spotlight-landscape",
+ edgeToEdge,
spotlight,
grid,
}));
@@ -1074,16 +1086,20 @@ export function createCallViewModel$(
const spotlightPortraitLayoutMedia$: Observable =
combineLatest([grid$, spotlight$], (grid, spotlight) => ({
type: "spotlight-portrait",
+ edgeToEdge: false,
spotlight,
grid,
}));
- const spotlightExpandedLayoutMedia$: Observable =
+ const spotlightExpandedLayoutMedia$ = (
+ edgeToEdge: boolean,
+ ): Observable =>
spotlightAndPip$.pipe(
switchMap(({ spotlight, pip$ }) =>
pip$.pipe(
map((pip) => ({
type: "spotlight-expanded" as const,
+ edgeToEdge,
spotlight,
pip: pip ?? undefined,
})),
@@ -1091,55 +1107,88 @@ export function createCallViewModel$(
),
);
- const oneOnOneLayoutMedia$: Observable =
- combineLatest([userMedia$, screenShares$]).pipe(
- switchMap(([userMedia, screenShares]) => {
- // One-on-one layout only supports 2 user media, no screen shares
- if (userMedia.length <= 2 && screenShares.length === 0) {
- const local = userMedia.find(
- (vm): vm is WrappedUserMediaViewModel & LocalUserMediaViewModel =>
- vm.type === "user" && vm.local,
+ const oneOnOneLayoutMedia$: Observable<{
+ local: LocalUserMediaViewModel;
+ remote: UserMediaViewModel | RingingMediaViewModel;
+ } | null> = combineLatest([userMedia$, screenShares$]).pipe(
+ switchMap(([userMedia, screenShares]) => {
+ // One-on-one layout only supports 2 user media, no screen shares
+ if (userMedia.length <= 2 && screenShares.length === 0) {
+ const local = userMedia.find(
+ (vm): vm is WrappedUserMediaViewModel & LocalUserMediaViewModel =>
+ vm.type === "user" && vm.local,
+ );
+
+ if (local !== undefined) {
+ const remote = userMedia.find(
+ (vm): vm is WrappedUserMediaViewModel & RemoteUserMediaViewModel =>
+ vm.type === "user" && !vm.local,
);
- if (local !== undefined) {
- const remote = userMedia.find(
- (
- vm,
- ): vm is WrappedUserMediaViewModel & RemoteUserMediaViewModel =>
- vm.type === "user" && !vm.local,
+ if (remote !== undefined) return of({ local, remote });
+
+ // If there's no other user media in the call (could still happen in
+ // this branch due to the duplicate tiles option), we could possibly
+ // show ringing media instead
+ if (userMedia.length === 1)
+ return ringingMedia$.pipe(
+ map((ringingMedia) => {
+ return ringingMedia.length === 1
+ ? {
+ local,
+ remote: ringingMedia[0],
+ }
+ : null;
+ }),
);
-
- if (remote !== undefined)
- return of({
- type: "one-on-one" as const,
- spotlight: remote,
- pip: local,
- });
-
- // If there's no other user media in the call (could still happen in
- // this branch due to the duplicate tiles option), we could possibly
- // show ringing media instead
- if (userMedia.length === 1)
- return ringingMedia$.pipe(
- map((ringingMedia) => {
- return ringingMedia.length === 1
- ? {
- type: "one-on-one" as const,
- spotlight: local,
- pip: ringingMedia[0],
- }
- : null;
- }),
- );
- }
}
+ }
- return of(null);
+ return of(null);
+ }),
+ );
+
+ const oneOnOneLandscapeLayoutMedia$: Observable =
+ oneOnOneLayoutMedia$.pipe(
+ map((media) => {
+ if (media === null) return null;
+ return media.remote.type === "ringing"
+ ? {
+ type: "one-on-one-landscape" as const,
+ edgeToEdge: false,
+ spotlight: media.local,
+ pip: media.remote,
+ }
+ : {
+ type: "one-on-one-landscape" as const,
+ edgeToEdge: false,
+ spotlight: media.remote,
+ pip: media.local,
+ };
+ }),
+ );
+
+ const oneOnOnePortraitLayoutMedia$: Observable =
+ oneOnOneLayoutMedia$.pipe(
+ switchMap((media) => {
+ if (media === null) return of(null);
+ return media.local.videoEnabled$.pipe(
+ map((videoEnabled) => ({
+ type: "one-on-one-portrait" as const,
+ edgeToEdge: true as const,
+ spotlight: media.remote,
+ pip: videoEnabled ? media.local : undefined,
+ })),
+ );
}),
);
const pipLayoutMedia$: Observable = spotlight$.pipe(
- map((spotlight) => ({ type: "pip", spotlight })),
+ map((spotlight) => ({
+ type: "pip",
+ edgeToEdge: platform !== "desktop",
+ spotlight,
+ })),
);
/**
@@ -1154,7 +1203,7 @@ export function createCallViewModel$(
switchMap((gridMode) => {
switch (gridMode) {
case "grid":
- return oneOnOneLayoutMedia$.pipe(
+ return oneOnOneLandscapeLayoutMedia$.pipe(
switchMap((oneOnOne) =>
oneOnOne === null ? gridLayoutMedia$ : of(oneOnOne),
),
@@ -1163,15 +1212,15 @@ export function createCallViewModel$(
return spotlightExpanded$.pipe(
switchMap((expanded) =>
expanded
- ? spotlightExpandedLayoutMedia$
- : spotlightLandscapeLayoutMedia$,
+ ? spotlightExpandedLayoutMedia$(false)
+ : spotlightLandscapeLayoutMedia$(false),
),
);
}
}),
);
case "narrow":
- return oneOnOneLayoutMedia$.pipe(
+ return oneOnOnePortraitLayoutMedia$.pipe(
switchMap((oneOnOne) =>
oneOnOne === null
? combineLatest([grid$, spotlight$], (grid, spotlight) =>
@@ -1180,9 +1229,7 @@ export function createCallViewModel$(
? spotlightPortraitLayoutMedia$
: gridLayoutMedia$,
).pipe(switchAll())
- : // The expanded spotlight layout makes for a better one-on-one
- // experience in narrow windows
- spotlightExpandedLayoutMedia$,
+ : of(oneOnOne),
),
);
case "flat":
@@ -1192,9 +1239,9 @@ export function createCallViewModel$(
case "grid":
// Yes, grid mode actually gets you a "spotlight" layout in
// this window mode.
- return spotlightLandscapeLayoutMedia$;
+ return spotlightLandscapeLayoutMedia$(true);
case "spotlight":
- return spotlightExpandedLayoutMedia$;
+ return spotlightExpandedLayoutMedia$(true);
}
}),
);
@@ -1205,6 +1252,201 @@ export function createCallViewModel$(
),
);
+ const showSpotlightIndicators$ = scope.behavior(
+ layoutMedia$.pipe(map((l) => l.type !== "grid")),
+ );
+
+ const showSpeakingIndicators$ = scope.behavior(
+ layoutMedia$.pipe(
+ map((l) => {
+ switch (l.type) {
+ case "spotlight-landscape":
+ case "spotlight-portrait":
+ // If the spotlight is showing the active speaker, we can do without
+ // speaking indicators as they're a redundant visual cue. But if
+ // screen sharing feeds are in the spotlight we still need them.
+ return l.spotlight.some((m) => m.type === "screen share");
+ // In expanded spotlight layout, the active speaker is always shown in
+ // the picture-in-picture tile so there is no need for speaking
+ // indicators. And in one-on-one layout there's no question as to who is
+ // speaking.
+ case "spotlight-expanded":
+ case "one-on-one-landscape":
+ case "one-on-one-portrait":
+ return false;
+ default:
+ return true;
+ }
+ }),
+ ),
+ );
+
+ const showNameTags$ = scope.behavior(
+ layoutMedia$.pipe(
+ switchMap((l) =>
+ l.type === "pip" || l.type === "one-on-one-portrait"
+ ? matrixRoomMembers$.pipe(
+ map(
+ (members) =>
+ // Hide name tags by default in these layouts. For safety we
+ // still need to show them in case it wouldn't be clear who
+ // the spotlight media belongs to.
+ // TODO: Respect io.element.functional_members (while still
+ // being careful to never show a functional member's media
+ // without a name tag!)
+ // TODO: Only hide name tags in DMs, not group chats that just
+ // happen to have only 2 users
+ members.size > 2,
+ ),
+ )
+ : of(true),
+ ),
+ ),
+ );
+
+ const toggleSpotlightExpanded$ = scope.behavior<(() => void) | null>(
+ windowMode$.pipe(
+ switchMap((mode) =>
+ mode === "normal"
+ ? layoutMedia$.pipe(
+ map(
+ (l) =>
+ l.type === "spotlight-landscape" ||
+ l.type === "spotlight-expanded",
+ ),
+ )
+ : of(false),
+ ),
+ distinctUntilChanged(),
+ map((enabled) =>
+ enabled ? (): void => spotlightExpandedToggle$.next() : null,
+ ),
+ ),
+ );
+
+ const edgeToEdge$ = scope.behavior(
+ layoutMedia$.pipe(map(({ edgeToEdge }) => edgeToEdge)),
+ );
+
+ const screenTap$ = new Subject();
+ const controlsTap$ = new Subject();
+ const screenHover$ = new Subject();
+ const screenUnhover$ = new Subject();
+
+ const naturallyShowFooter$ = scope.behavior(
+ edgeToEdge$.pipe(
+ switchMap((edgeToEdge) => {
+ if (!edgeToEdge) return of(true);
+
+ // Sadly Firefox has some layering glitches that prevent the footer
+ // from appearing properly. They happen less often if we never hide
+ // the footer.
+ if (isFirefox()) return of(true);
+
+ // Layout is edge-to-edge; show/hide the footer in response to interactions
+ return windowMode$.pipe(
+ switchMap((mode) => {
+ if (mode === "pip" && platform !== "desktop") {
+ // No controls are shown in mobile pip as interactions are disabled
+ return of(false);
+ }
+ const showInitially = mode !== "flat";
+ const timeout$ = mode === "flat" ? timer(showFooterMs) : NEVER;
+
+ return merge(
+ screenTap$.pipe(map(() => "tap screen" as const)),
+ controlsTap$.pipe(map(() => "tap controls" as const)),
+ screenHover$.pipe(map(() => "hover" as const)),
+ ).pipe(
+ switchScan((state, interaction) => {
+ switch (interaction) {
+ case "tap screen":
+ return state
+ ? // Toggle visibility on tap
+ of(false)
+ : // Hide after a timeout
+ timeout$.pipe(
+ map(() => false),
+ startWith(true),
+ );
+ case "tap controls":
+ // The user is interacting with things, so reset the timeout
+ return timeout$.pipe(
+ map(() => false),
+ startWith(true),
+ );
+ case "hover":
+ // Show on hover and hide after a timeout
+ return race(timeout$, screenUnhover$.pipe(take(1))).pipe(
+ map(() => false),
+ startWith(true),
+ );
+ }
+ }, showInitially),
+ startWith(showInitially),
+ );
+ }),
+ );
+ }),
+ ),
+ );
+
+ const urlParams = getUrlParams();
+ const showFooterUrlParams = !(
+ urlParams.header === HeaderStyle.None && urlParams.showControls === false
+ );
+ const showFooter$ = scope.behavior(
+ naturallyShowFooter$.pipe(
+ map((naturallyShowFooter) => naturallyShowFooter && showFooterUrlParams),
+ ),
+ );
+ const settingsOpen$ = new BehaviorSubject(false);
+ const setSettingsOpen$ = constant((open: boolean) => {
+ settingsOpen$.next(open);
+ });
+
+ const showHeader$ = scope.behavior(
+ windowMode$.pipe(
+ switchMap((mode) => {
+ // In small windows the header would be too obstructive
+ if (mode === "pip" || mode === "flat") return of(false);
+ // In edge-to-edge layouts, couple the visibility of the header
+ // to that of the footer
+ return edgeToEdge$.pipe(
+ switchMap((edgeToEdge) => (edgeToEdge ? showFooter$ : of(true))),
+ );
+ }),
+ ),
+ );
+
+ /**
+ * The alignment of the floating spotlight tile, if present.
+ */
+ const spotlightAlignment$ = new BehaviorSubject({
+ inline: "end",
+ block: "end",
+ });
+ /**
+ * The size of the small picture-in-picture tile, if present, when in portrait.
+ */
+ const portraitPipSize$ = scope.behavior(
+ showFooter$.pipe(map((showFooter) => (showFooter ? "lg" : "sm"))),
+ );
+ /**
+ * The alignment of the small picture-in-picture tile, if present, when in portrait.
+ */
+ const portraitPipAlignment$ = new BehaviorSubject({
+ inline: "end",
+ block: "end",
+ });
+ /**
+ * The alignment of the small picture-in-picture tile, if present, when in landscape.
+ */
+ const landscapePipAlignment$ = new BehaviorSubject({
+ inline: "end",
+ block: "start",
+ });
+
// There is a cyclical dependency here: the layout algorithms want to know
// which tiles are on screen, but to know which tiles are on screen we have to
// first render a layout. To deal with this we assume initially that no tiles
@@ -1231,16 +1473,33 @@ export function createCallViewModel$(
case "spotlight-portrait":
[layout, newTiles] = gridLikeLayout(
media,
+ spotlightAlignment$,
visibleTiles,
setVisibleTiles,
prevTiles,
);
break;
case "spotlight-expanded":
- [layout, newTiles] = spotlightExpandedLayout(media, prevTiles);
+ [layout, newTiles] = spotlightExpandedLayout(
+ media,
+ landscapePipAlignment$,
+ prevTiles,
+ );
break;
- case "one-on-one":
- [layout, newTiles] = oneOnOneLayout(media, prevTiles);
+ case "one-on-one-landscape":
+ [layout, newTiles] = oneOnOneLandscapeLayout(
+ media,
+ landscapePipAlignment$,
+ prevTiles,
+ );
+ break;
+ case "one-on-one-portrait":
+ [layout, newTiles] = oneOnOnePortraitLayout(
+ media,
+ portraitPipSize$,
+ portraitPipAlignment$,
+ prevTiles,
+ );
break;
case "pip":
[layout, newTiles] = pipLayout(media, prevTiles);
@@ -1268,130 +1527,6 @@ export function createCallViewModel$(
layoutInternals$.pipe(map(({ tiles }) => tiles.generation)),
);
- const showSpotlightIndicators$ = scope.behavior(
- layout$.pipe(map((l) => l.type !== "grid")),
- );
-
- const showSpeakingIndicators$ = scope.behavior(
- layout$.pipe(
- switchMap((l) => {
- switch (l.type) {
- case "spotlight-landscape":
- case "spotlight-portrait":
- // If the spotlight is showing the active speaker, we can do without
- // speaking indicators as they're a redundant visual cue. But if
- // screen sharing feeds are in the spotlight we still need them.
- return l.spotlight.media$.pipe(
- map((models: MediaViewModel[]) =>
- models.some((m) => m.type === "screen share"),
- ),
- );
- // In expanded spotlight layout, the active speaker is always shown in
- // the picture-in-picture tile so there is no need for speaking
- // indicators. And in one-on-one layout there's no question as to who is
- // speaking.
- case "spotlight-expanded":
- case "one-on-one":
- return of(false);
- default:
- return of(true);
- }
- }),
- ),
- );
-
- const toggleSpotlightExpanded$ = scope.behavior<(() => void) | null>(
- windowMode$.pipe(
- switchMap((mode) =>
- mode === "normal"
- ? layout$.pipe(
- map(
- (l) =>
- l.type === "spotlight-landscape" ||
- l.type === "spotlight-expanded",
- ),
- )
- : of(false),
- ),
- distinctUntilChanged(),
- map((enabled) =>
- enabled ? (): void => spotlightExpandedToggle$.next() : null,
- ),
- ),
- );
-
- const screenTap$ = new Subject();
- const controlsTap$ = new Subject();
- const screenHover$ = new Subject();
- const screenUnhover$ = new Subject();
-
- const showHeader$ = scope.behavior(
- windowMode$.pipe(map((mode) => mode !== "pip" && mode !== "flat")),
- );
-
- const urlParams = getUrlParams();
- const showFooterUrlParams = !(
- urlParams.header === HeaderStyle.None && urlParams.showControls === false
- );
- const showFooterLayout$ = scope.behavior(
- windowMode$.pipe(
- switchMap((mode) => {
- switch (mode) {
- case "pip":
- return of(platform === "desktop" ? true : false);
- case "normal":
- case "narrow":
- return of(true);
- case "flat":
- // Sadly Firefox has some layering glitches that prevent the footer
- // from appearing properly. They happen less often if we never hide
- // the footer.
- if (isFirefox()) return of(true);
- // Show/hide the footer in response to interactions
- return merge(
- screenTap$.pipe(map(() => "tap screen" as const)),
- controlsTap$.pipe(map(() => "tap controls" as const)),
- screenHover$.pipe(map(() => "hover" as const)),
- ).pipe(
- switchScan((state, interaction) => {
- switch (interaction) {
- case "tap screen":
- return state
- ? // Toggle visibility on tap
- of(false)
- : // Hide after a timeout
- timer(showFooterMs).pipe(
- map(() => false),
- startWith(true),
- );
- case "tap controls":
- // The user is interacting with things, so reset the timeout
- return timer(showFooterMs).pipe(
- map(() => false),
- startWith(true),
- );
- case "hover":
- // Show on hover and hide after a timeout
- return race(
- timer(showFooterMs),
- screenUnhover$.pipe(take(1)),
- ).pipe(
- map(() => false),
- startWith(true),
- );
- }
- }, false),
- startWith(false),
- );
- }
- }),
- ),
- );
- const showFooter$ = scope.behavior(
- showFooterLayout$.pipe(
- map((showFooter) => showFooter && showFooterUrlParams),
- ),
- );
/**
* Whether audio is currently being output through the earpiece.
*/
@@ -1595,7 +1730,6 @@ export function createCallViewModel$(
audibleReactions$: audibleReactions$,
visibleReactions$: visibleReactions$,
- windowMode$: windowMode$,
spotlightExpanded$: spotlightExpanded$,
toggleSpotlightExpanded$: toggleSpotlightExpanded$,
gridMode$: gridMode$,
@@ -1621,8 +1755,12 @@ export function createCallViewModel$(
tileStoreGeneration$: tileStoreGeneration$,
showSpotlightIndicators$: showSpotlightIndicators$,
showSpeakingIndicators$: showSpeakingIndicators$,
+ showNameTags$,
showHeader$: showHeader$,
showFooter$: showFooter$,
+ settingsOpen$: settingsOpen$,
+ setSettingsOpen$: setSettingsOpen$,
+ edgeToEdge$,
earpieceMode$: earpieceMode$,
audioOutputSwitcher$: audioOutputSwitcher$,
reconnecting$: localMembership.reconnecting$,
diff --git a/src/state/CallViewModel/CallViewModelTestUtils.ts b/src/state/CallViewModel/CallViewModelTestUtils.ts
index 9685c709..3155eb11 100644
--- a/src/state/CallViewModel/CallViewModelTestUtils.ts
+++ b/src/state/CallViewModel/CallViewModelTestUtils.ts
@@ -8,11 +8,11 @@ Please see LICENSE in the repository root for full details.
import {
ConnectionState,
- type LocalParticipant,
type Participant,
ParticipantEvent,
type RemoteParticipant,
type Room as LivekitRoom,
+ type TrackPublication,
} from "livekit-client";
import { SyncState } from "matrix-js-sdk/lib/sync";
import { BehaviorSubject, combineLatest, map, of } from "rxjs";
@@ -72,6 +72,7 @@ export interface CallViewModelInputs {
roomMembers: RoomMember[];
livekitConnectionState$: Behavior;
speaking: Map>;
+ videoEnabled: Map>;
sharingScreen: Map>;
mediaDevices: MediaDevices;
initialSyncState: SyncState;
@@ -98,6 +99,7 @@ export function withCallViewModel(mode: MatrixRTCMode) {
ConnectionState.Connected,
),
speaking = new Map(),
+ videoEnabled = new Map(),
sharingScreen = new Map(),
mediaDevices = mockMediaDevices({}),
initialSyncState = SyncState.Syncing,
@@ -151,11 +153,19 @@ export function withCallViewModel(mode: MatrixRTCMode) {
.mockReturnValue(remoteParticipants$);
const mediaSpy = vi
.spyOn(ComponentsCore, "observeParticipantMedia")
- .mockImplementation((p) =>
- of({ participant: p } as Partial<
- ComponentsCore.ParticipantMedia
- > as ComponentsCore.ParticipantMedia),
- );
+ .mockImplementation((p) => {
+ return (videoEnabled.get(p) ?? constant(false)).pipe(
+ map((videoEnabled) => ({
+ participant: p,
+ isMicrophoneEnabled: false,
+ isCameraEnabled: videoEnabled,
+ isScreenShareEnabled: false,
+ cameraTrack: {
+ isMuted: !videoEnabled,
+ } as unknown as TrackPublication,
+ })),
+ );
+ });
const eventsSpy = vi
.spyOn(ComponentsCore, "observeParticipantEvents")
.mockImplementation((p, ...eventTypes) => {
diff --git a/src/state/GridLikeLayout.ts b/src/state/GridLikeLayout.ts
index 0d130834..f91f8e31 100644
--- a/src/state/GridLikeLayout.ts
+++ b/src/state/GridLikeLayout.ts
@@ -5,7 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
-import { type Layout, type LayoutMedia } from "./layout-types.ts";
+import { type BehaviorSubject } from "rxjs";
+
+import {
+ type Alignment,
+ type Layout,
+ type LayoutMedia,
+} from "./layout-types.ts";
import { type TileStore } from "./TileStore";
export type GridLikeLayoutType =
@@ -19,6 +25,7 @@ export type GridLikeLayoutType =
*/
export function gridLikeLayout(
media: LayoutMedia & { type: GridLikeLayoutType },
+ spotlightAlignment$: BehaviorSubject,
visibleTiles: number,
setVisibleTiles: (value: number) => void,
prevTiles: TileStore,
@@ -37,6 +44,7 @@ export function gridLikeLayout(
type: media.type,
spotlight: tiles.spotlightTile,
grid: tiles.gridTiles,
+ spotlightAlignment$,
setVisibleTiles,
} as Layout & { type: GridLikeLayoutType },
tiles,
diff --git a/src/state/OneOnOneLayout.ts b/src/state/OneOnOneLandscapeLayout.ts
similarity index 55%
rename from src/state/OneOnOneLayout.ts
rename to src/state/OneOnOneLandscapeLayout.ts
index 27fa4439..4198ff03 100644
--- a/src/state/OneOnOneLayout.ts
+++ b/src/state/OneOnOneLandscapeLayout.ts
@@ -1,29 +1,39 @@
/*
Copyright 2024 New Vector Ltd.
+Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
-import { type OneOnOneLayout, type OneOnOneLayoutMedia } from "./layout-types";
+import { type BehaviorSubject } from "rxjs";
+
+import {
+ type Alignment,
+ type OneOnOneLandscapeLayout,
+ type OneOnOneLandscapeLayoutMedia,
+} from "./layout-types";
import { type TileStore } from "./TileStore";
/**
- * Produces a one-on-one layout with the given media.
+ * Produces a one-on-one landscape layout with the given media.
*/
-export function oneOnOneLayout(
- media: OneOnOneLayoutMedia,
+export function oneOnOneLandscapeLayout(
+ media: OneOnOneLandscapeLayoutMedia,
+ pipAlignment$: BehaviorSubject,
prevTiles: TileStore,
-): [OneOnOneLayout, TileStore] {
+): [OneOnOneLandscapeLayout, TileStore] {
const update = prevTiles.from(2);
update.registerGridTile(media.pip);
update.registerGridTile(media.spotlight);
const tiles = update.build();
+
return [
{
type: media.type,
spotlight: tiles.gridTilesByMedia.get(media.spotlight)!,
pip: tiles.gridTilesByMedia.get(media.pip)!,
+ pipAlignment$,
},
tiles,
];
diff --git a/src/state/OneOnOnePortraitLayout.ts b/src/state/OneOnOnePortraitLayout.ts
new file mode 100644
index 00000000..9be80421
--- /dev/null
+++ b/src/state/OneOnOnePortraitLayout.ts
@@ -0,0 +1,43 @@
+/*
+Copyright 2024 New Vector Ltd.
+Copyright 2026 Element Creations Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE in the repository root for full details.
+*/
+
+import { type BehaviorSubject } from "rxjs";
+
+import {
+ type Alignment,
+ type OneOnOnePortraitLayout,
+ type OneOnOnePortraitLayoutMedia,
+} from "./layout-types";
+import { type TileStore } from "./TileStore";
+import { type Behavior } from "./Behavior";
+
+/**
+ * Produces a one-on-one portrait layout with the given media.
+ */
+export function oneOnOnePortraitLayout(
+ media: OneOnOnePortraitLayoutMedia,
+ pipSize$: Behavior<"sm" | "lg">,
+ pipAlignment$: BehaviorSubject,
+ prevTiles: TileStore,
+): [OneOnOnePortraitLayout, TileStore] {
+ const update = prevTiles.from(media.pip === undefined ? 0 : 1);
+ update.registerSpotlight([media.spotlight], true);
+ if (media.pip !== undefined) update.registerGridTile(media.pip);
+ const tiles = update.build();
+
+ return [
+ {
+ type: media.type,
+ spotlight: tiles.spotlightTile!,
+ pip: media.pip && tiles.gridTilesByMedia.get(media.pip),
+ pipSize$,
+ pipAlignment$,
+ },
+ tiles,
+ ];
+}
diff --git a/src/state/SpotlightExpandedLayout.ts b/src/state/SpotlightExpandedLayout.ts
index 9dc2c815..59ab8ab9 100644
--- a/src/state/SpotlightExpandedLayout.ts
+++ b/src/state/SpotlightExpandedLayout.ts
@@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
+import { type BehaviorSubject } from "rxjs";
+
import {
+ type Alignment,
type SpotlightExpandedLayout,
type SpotlightExpandedLayoutMedia,
} from "./layout-types";
@@ -16,6 +19,7 @@ import { type TileStore } from "./TileStore";
*/
export function spotlightExpandedLayout(
media: SpotlightExpandedLayoutMedia,
+ pipAlignment$: BehaviorSubject,
prevTiles: TileStore,
): [SpotlightExpandedLayout, TileStore] {
const update = prevTiles.from(1);
@@ -27,7 +31,8 @@ export function spotlightExpandedLayout(
{
type: media.type,
spotlight: tiles.spotlightTile!,
- pip: tiles.gridTiles[0],
+ pip: tiles.gridTiles.at(0),
+ pipAlignment$,
},
tiles,
];
diff --git a/src/state/ViewModel.ts b/src/state/ViewModel.ts
new file mode 100644
index 00000000..9d245b71
--- /dev/null
+++ b/src/state/ViewModel.ts
@@ -0,0 +1,49 @@
+/*
+Copyright 2026 Element Software Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE in the repository root for full details.
+*/
+
+import { BehaviorSubject } from "rxjs";
+import { useState, useEffect } from "react";
+
+import { type Behavior } from "./Behavior";
+
+export type ViewModel = {
+ [K in keyof Snapshot as `${string & K}$`]: Behavior;
+};
+
+/**
+ * This allows to build a view model (or Partial view model)
+ * with BehaviorSubjects.
+ * It can be used in tests and for simplifying view model creation for non reactive snapshot parameters.
+ *
+ * @param snapshot The snapshot values this view model with start with. ({a: number, b: string})
+ * @returns A view model: ({a$: BehaviroSubject, b$: BehaviroSubject}) (note the automatic addition of $ at the end of the keys)
+ */
+export function createStaticViewModel(
+ snapshot: Snapshot,
+): ViewModel {
+ const vm = {} as ViewModel;
+ for (const key in snapshot) {
+ (vm as Record>)[`${key}$`] = new BehaviorSubject(
+ snapshot[key],
+ );
+ }
+ return vm;
+}
+
+export function useStaticViewModel(
+ snapshot: Snapshot,
+): ViewModel {
+ const [vm] = useState(() => createStaticViewModel(snapshot));
+ useEffect(() => {
+ for (const key in snapshot) {
+ (vm as unknown as Record>)[
+ `${key}$`
+ ].next(snapshot[key]);
+ }
+ }, [snapshot, vm]);
+ return vm;
+}
diff --git a/src/state/layout-types.ts b/src/state/layout-types.ts
index 2e779057..2b0d459d 100644
--- a/src/state/layout-types.ts
+++ b/src/state/layout-types.ts
@@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
+import { type BehaviorSubject } from "rxjs";
+
import { type LocalUserMediaViewModel } from "./media/LocalUserMediaViewModel.ts";
import { type MediaViewModel } from "./media/MediaViewModel.ts";
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel.ts";
@@ -13,39 +15,53 @@ import {
type GridTileViewModel,
type SpotlightTileViewModel,
} from "./TileViewModel.ts";
+import { type Behavior } from "./Behavior.ts";
export interface GridLayoutMedia {
type: "grid";
+ edgeToEdge: false;
spotlight?: MediaViewModel[];
grid: UserMediaViewModel[];
}
export interface SpotlightLandscapeLayoutMedia {
type: "spotlight-landscape";
+ edgeToEdge: boolean;
spotlight: MediaViewModel[];
grid: UserMediaViewModel[];
}
export interface SpotlightPortraitLayoutMedia {
type: "spotlight-portrait";
+ edgeToEdge: false;
spotlight: MediaViewModel[];
grid: UserMediaViewModel[];
}
export interface SpotlightExpandedLayoutMedia {
type: "spotlight-expanded";
+ edgeToEdge: boolean;
spotlight: MediaViewModel[];
pip?: UserMediaViewModel;
}
-export interface OneOnOneLayoutMedia {
- type: "one-on-one";
+export interface OneOnOneLandscapeLayoutMedia {
+ type: "one-on-one-landscape";
+ edgeToEdge: false;
spotlight: UserMediaViewModel;
pip: LocalUserMediaViewModel | RingingMediaViewModel;
}
+export interface OneOnOnePortraitLayoutMedia {
+ type: "one-on-one-portrait";
+ edgeToEdge: true;
+ spotlight: UserMediaViewModel | RingingMediaViewModel;
+ pip?: LocalUserMediaViewModel;
+}
+
export interface PipLayoutMedia {
type: "pip";
+ edgeToEdge: boolean;
spotlight: MediaViewModel[];
}
@@ -54,13 +70,20 @@ export type LayoutMedia =
| SpotlightLandscapeLayoutMedia
| SpotlightPortraitLayoutMedia
| SpotlightExpandedLayoutMedia
- | OneOnOneLayoutMedia
+ | OneOnOneLandscapeLayoutMedia
+ | OneOnOnePortraitLayoutMedia
| PipLayoutMedia;
+export interface Alignment {
+ inline: "start" | "end";
+ block: "start" | "end";
+}
+
export interface GridLayout {
type: "grid";
spotlight?: SpotlightTileViewModel;
grid: GridTileViewModel[];
+ spotlightAlignment$: BehaviorSubject;
setVisibleTiles: (value: number) => void;
}
@@ -82,12 +105,22 @@ export interface SpotlightExpandedLayout {
type: "spotlight-expanded";
spotlight: SpotlightTileViewModel;
pip?: GridTileViewModel;
+ pipAlignment$: BehaviorSubject;
}
-export interface OneOnOneLayout {
- type: "one-on-one";
+export interface OneOnOneLandscapeLayout {
+ type: "one-on-one-landscape";
spotlight: GridTileViewModel;
pip: GridTileViewModel;
+ pipAlignment$: BehaviorSubject;
+}
+
+export interface OneOnOnePortraitLayout {
+ type: "one-on-one-portrait";
+ spotlight: SpotlightTileViewModel;
+ pip?: GridTileViewModel;
+ pipSize$: Behavior<"sm" | "lg">;
+ pipAlignment$: BehaviorSubject;
}
export interface PipLayout {
@@ -104,5 +137,6 @@ export type Layout =
| SpotlightLandscapeLayout
| SpotlightPortraitLayout
| SpotlightExpandedLayout
- | OneOnOneLayout
+ | OneOnOneLandscapeLayout
+ | OneOnOnePortraitLayout
| PipLayout;
diff --git a/src/tile/GridTile.module.css b/src/tile/GridTile.module.css
index ee605e46..7ffe67d4 100644
--- a/src/tile/GridTile.module.css
+++ b/src/tile/GridTile.module.css
@@ -72,6 +72,10 @@ borders don't support gradients */
}
}
+.tile.edgeToEdge {
+ --media-view-border-radius: 0;
+}
+
.muteIcon[data-muted="true"] {
color: var(--cpd-color-icon-secondary);
}
diff --git a/src/tile/GridTile.test.tsx b/src/tile/GridTile.test.tsx
index 501f440c..2a169cb0 100644
--- a/src/tile/GridTile.test.tsx
+++ b/src/tile/GridTile.test.tsx
@@ -77,6 +77,7 @@ test("GridTile is accessible", async () => {
targetWidth={300}
targetHeight={200}
showSpeakingIndicators
+ showNameTags
focusable
/>
,
@@ -109,6 +110,7 @@ test("GridTile displays ringing media", async () => {
targetWidth={300}
targetHeight={200}
showSpeakingIndicators
+ showNameTags
focusable
/>
,
diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx
index 13cf677f..88754b9d 100644
--- a/src/tile/GridTile.tsx
+++ b/src/tile/GridTile.tsx
@@ -62,6 +62,7 @@ interface TileProps {
targetHeight: number;
displayName: string;
mxcAvatarUrl: string | undefined;
+ showNameTags: boolean;
focusable: boolean;
}
@@ -398,6 +399,7 @@ interface GridTileProps {
className?: string;
style?: ComponentProps["style"];
showSpeakingIndicators: boolean;
+ showNameTags: boolean;
focusable: boolean;
}
@@ -419,9 +421,9 @@ export const GridTile: FC = ({
);
} else if (media.local) {
diff --git a/src/tile/MediaView.module.css b/src/tile/MediaView.module.css
index 49199253..240f14d1 100644
--- a/src/tile/MediaView.module.css
+++ b/src/tile/MediaView.module.css
@@ -85,6 +85,7 @@ unconditionally select the container so we can use cqmin units */
calc(var(--media-view-border-radius) - var(--cpd-space-3x))
);
padding: var(--fg-inset);
+ transition: padding 0.3s;
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr auto;
@@ -94,6 +95,12 @@ unconditionally select the container so we can use cqmin units */
contain: strict;
}
+@media (prefers-reduced-motion) {
+ .fg {
+ transition: none;
+ }
+}
+
.nameTag {
grid-area: nameTag;
place-self: end start;
diff --git a/src/tile/MediaView.test.tsx b/src/tile/MediaView.test.tsx
index 6ef5eb7e..c7881976 100644
--- a/src/tile/MediaView.test.tsx
+++ b/src/tile/MediaView.test.tsx
@@ -42,6 +42,7 @@ describe("MediaView", () => {
targetHeight: 200,
mirror: false,
unencryptedWarning: false,
+ showNameTags: true,
video: trackReference,
userId: "@alice:example.com",
mxcAvatarUrl: undefined,
@@ -107,6 +108,16 @@ describe("MediaView", () => {
expect(screen.getByRole("img", { name: "Not encrypted" })).toBeTruthy();
});
+ test("is shown and accessible even with name tag hidden", async () => {
+ const { container } = render(
+
+
+ ,
+ );
+ expect(await axe(container)).toHaveNoViolations();
+ screen.getByRole("img", { name: "Not encrypted" });
+ });
+
test("is not shown", () => {
render(
diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx
index eb6cc6b4..6ff97f7a 100644
--- a/src/tile/MediaView.tsx
+++ b/src/tile/MediaView.tsx
@@ -44,6 +44,7 @@ interface Props extends ComponentProps {
videoEnabled: boolean;
unencryptedWarning: boolean;
status?: { text: string; Icon: ComponentType> };
+ showNameTags: boolean;
nameTagLeadingIcon?: ReactNode;
displayName: string;
mxcAvatarUrl: string | undefined;
@@ -72,6 +73,7 @@ export const MediaView: FC = ({
userId,
videoEnabled,
unencryptedWarning,
+ showNameTags,
nameTagLeadingIcon,
displayName,
mxcAvatarUrl,
@@ -94,6 +96,23 @@ export const MediaView: FC = ({
const avatarSize = Math.round(Math.min(targetWidth, targetHeight) / 2);
+ const warnings = unencryptedWarning && (
+
+
+
+ );
+
return (
= ({
)*/}
-
- {nameTagLeadingIcon}
-
- {displayName}
-
- {unencryptedWarning && (
- = 100 ? (
+
+ {nameTagLeadingIcon}
+
-
-
- )}
-
+ {displayName}
+
+ {warnings}
+
+ ) : (
+ warnings
+ )}
{primaryButton}
diff --git a/src/tile/SpotlightTile.module.css b/src/tile/SpotlightTile.module.css
index 037cf10d..54e31106 100644
--- a/src/tile/SpotlightTile.module.css
+++ b/src/tile/SpotlightTile.module.css
@@ -35,7 +35,9 @@ Please see LICENSE in the repository root for full details.
.maximised .item {
/* Ensure that foreground elements lie within the safe area */
- --media-view-fg-inset: 10px calc(env(safe-area-inset-right) + 10px) 10px
+ --media-view-fg-inset: calc(var(--call-view-safe-area-inset-top, 0px) + 10px)
+ calc(env(safe-area-inset-right) + 10px)
+ calc(var(--call-view-safe-area-inset-bottom, 0px) + 10px)
calc(env(safe-area-inset-left) + 10px);
}
diff --git a/src/tile/SpotlightTile.test.tsx b/src/tile/SpotlightTile.test.tsx
index 533c3b2f..ea987007 100644
--- a/src/tile/SpotlightTile.test.tsx
+++ b/src/tile/SpotlightTile.test.tsx
@@ -65,6 +65,7 @@ test("SpotlightTile is accessible", async () => {
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
+ showNameTags
focusable={true}
/>,
);
@@ -106,6 +107,7 @@ test("Screen share volume UI is shown when screen share has audio", async () =>
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
+ showNameTags
focusable
/>
,
@@ -135,6 +137,7 @@ test("Screen share volume UI is hidden when screen share has no audio", async ()
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
+ showNameTags
focusable
/>,
);
@@ -171,6 +174,7 @@ test("SpotlightTile displays ringing media", async () => {
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
+ showNameTags
focusable={true}
/>,
);
diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx
index 808773b0..09587497 100644
--- a/src/tile/SpotlightTile.tsx
+++ b/src/tile/SpotlightTile.tsx
@@ -66,6 +66,7 @@ interface SpotlightItemBaseProps {
userId: string;
displayName: string;
mxcAvatarUrl: string | undefined;
+ showNameTags: boolean;
focusable: boolean;
"aria-hidden"?: boolean;
}
@@ -244,6 +245,7 @@ interface SpotlightItemProps {
* The height this tile will have once its animations have settled.
*/
targetHeight: number;
+ showNameTags: boolean;
focusable: boolean;
intersectionObserver$: Observable;
/**
@@ -258,6 +260,7 @@ const SpotlightItem: FC = ({
vm,
targetWidth,
targetHeight,
+ showNameTags,
focusable,
intersectionObserver$,
snap,
@@ -293,6 +296,7 @@ const SpotlightItem: FC = ({
userId: vm.userId,
displayName,
mxcAvatarUrl,
+ showNameTags,
focusable,
"aria-hidden": ariaHidden,
};
@@ -381,6 +385,7 @@ interface Props {
targetWidth: number;
targetHeight: number;
showIndicators: boolean;
+ showNameTags: boolean;
focusable: boolean;
className?: string;
style?: ComponentProps["style"];
@@ -394,6 +399,7 @@ export const SpotlightTile: FC = ({
targetWidth,
targetHeight,
showIndicators,
+ showNameTags,
focusable = true,
className,
style,
@@ -504,6 +510,7 @@ export const SpotlightTile: FC = ({
vm={vm}
targetWidth={targetWidth}
targetHeight={targetHeight}
+ showNameTags={showNameTags}
focusable={focusable}
intersectionObserver$={intersectionObserver$}
// This is how we get the container to scroll to the right media
diff --git a/src/utils/test-viewmodel.ts b/src/utils/test-viewmodel.ts
index b5438371..7c670308 100644
--- a/src/utils/test-viewmodel.ts
+++ b/src/utils/test-viewmodel.ts
@@ -39,6 +39,9 @@ import { aliceRtcMember, localRtcMember } from "./test-fixtures";
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
import { constant } from "../state/Behavior";
import { MatrixRTCMode } from "../settings/settings";
+import { createCallFooterViewModel } from "../components/CallFooterViewModel";
+import { type FooterSnapshot } from "../components/CallFooter";
+import { type ViewModel } from "../state/ViewModel";
mockConfig({ livekit: { livekit_service_url: "https://example.com" } });
@@ -136,6 +139,7 @@ export function getBasicCallViewModelEnvironment(
callViewModelOptions: Partial = {},
): {
vm: CallViewModel;
+ footerVm: ViewModel;
rtcMemberships$: BehaviorSubject;
rtcSession: MockRTCSession;
handRaisedSubject$: BehaviorSubject>;
@@ -148,12 +152,15 @@ export function getBasicCallViewModelEnvironment(
const handRaisedSubject$ = new BehaviorSubject({});
const reactionsSubject$ = new BehaviorSubject({});
+ const scope = testScope();
+ const muteStates = mockMuteStates();
+ const mediaDevices = mediaDevicesOverride ?? mockMediaDevices({});
const vm = createCallViewModel$(
- testScope(),
+ scope,
rtcSession.asMockedSession(),
matrixRoom,
- mediaDevicesOverride ?? mockMediaDevices({}),
- mockMuteStates(),
+ mediaDevices,
+ muteStates,
{
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
livekitRoomFactory: (): LivekitRoom =>
@@ -171,8 +178,16 @@ export function getBasicCallViewModelEnvironment(
reactionsSubject$,
constant({ processor: undefined, supported: false }),
);
+ const footerVm = createCallFooterViewModel(
+ testScope(),
+ vm,
+ muteStates,
+ mediaDevices,
+ "reactionId",
+ );
return {
vm,
+ footerVm,
rtcMemberships$,
rtcSession,
handRaisedSubject$: handRaisedSubject$,
diff --git a/src/widget.ts b/src/widget.ts
index 2ec76e15..462fc6e0 100644
--- a/src/widget.ts
+++ b/src/widget.ts
@@ -61,7 +61,7 @@ export interface WidgetHelpers {
* is initialized with `initializeWidget`. This should happen at the top level because the widget messaging
* needs to be set up ASAP on load to ensure it doesn't miss any requests.
*/
-export let widget: WidgetHelpers | null;
+export let widget: WidgetHelpers | null = null;
/**
* Should be called as soon as possible on app start. (In the initilizer before react)
diff --git a/vite.config.ts b/vite.config.ts
index d051bba0..6d224b7e 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -24,18 +24,10 @@ import react from "@vitejs/plugin-react";
import { realpathSync } from "fs";
import * as fs from "node:fs";
-// https://vitejs.dev/config/
-// Modified type helper from defineConfig to allow for packageType (see defineConfig from vite)
-export default ({
+export const vitePluginsConfig = ({
mode,
- packageType,
-}: ConfigEnv & { packageType?: "full" | "embedded" }): UserConfig => {
+}: Pick): UserConfig => {
const env = loadEnv(mode, process.cwd());
- // Environment variables with the VITE_ prefix are accessible at runtime.
- // So, we set this to allow for build/package specific behavior.
- // In future we might be able to do what is needed via code splitting at
- // build time.
- process.env.VITE_PACKAGE = packageType ?? "full";
const plugins: PluginOption[] = [
react(),
wasm(),
@@ -72,7 +64,7 @@ export default ({
);
}
- if (!process.env.STORYBOOK) {
+ if (!process.env.STORYBOOK && !process.env.VITEST) {
plugins.push(
createHtmlPlugin({
entry: "src/main.tsx",
@@ -86,6 +78,20 @@ export default ({
);
}
+ return { plugins };
+};
+// https://vitejs.dev/config/
+// Modified type helper from defineConfig to allow for packageType (see defineConfig from vite)
+export default ({
+ mode,
+ packageType,
+}: ConfigEnv & { packageType?: "full" | "embedded" }): UserConfig => {
+ // Environment variables with the VITE_ prefix are accessible at runtime.
+ // So, we set this to allow for build/package specific behavior.
+ // In future we might be able to do what is needed via code splitting at
+ // build time.
+ process.env.VITE_PACKAGE = packageType ?? "full";
+
// The crypto WASM module is imported dynamically. Since it's common
// for developers to use a linked copy of matrix-js-sdk or Rust
// crypto (which could reside anywhere on their file system), Vite
@@ -102,6 +108,7 @@ export default ({
console.log("Allowed vite paths:", allow);
return {
+ ...vitePluginsConfig({ mode }),
server: {
host: true,
port: 3000,
@@ -137,7 +144,6 @@ export default ({
},
},
},
- plugins,
resolve: {
alias: {
// matrix-widget-api has its transpiled lib/index.js as its entry point,
diff --git a/vitest.config.ts b/vitest.config.ts
index 90082f58..2dc9382c 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,20 +1,54 @@
import { defineConfig, mergeConfig } from "vitest/config";
+import { playwright } from "@vitest/browser-playwright";
+import { vitePluginsConfig } from "./vite.config";
+import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
-import viteConfig from "./vite.config";
+const dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig((configEnv) =>
mergeConfig(
- viteConfig(configEnv),
+ vitePluginsConfig(configEnv),
defineConfig({
test: {
- environment: "jsdom",
- css: {
- modules: {
- classNameStrategy: "non-scoped",
+ fileParallelism: true,
+ projects: [
+ {
+ extends: true,
+ test: {
+ name: "unit",
+ css: {
+ include: /.+/,
+ modules: {
+ classNameStrategy: "non-scoped",
+ },
+ },
+ setupFiles: ["src/vitest.setup.ts"],
+ environment: "jsdom",
+ include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
+ },
},
- },
- setupFiles: ["src/vitest.setup.ts"],
- include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
+ {
+ plugins: [
+ storybookTest({
+ // The location of your Storybook config, main.js|ts
+ configDir: "./.storybook",
+ }),
+ ...vitePluginsConfig(configEnv).plugins!,
+ ],
+ test: {
+ name: "storybook",
+ browser: {
+ enabled: true,
+ // Make sure to install Playwright
+ provider: playwright(),
+ headless: true,
+ instances: [{ browser: "chromium" }],
+ },
+ },
+ },
+ ],
coverage: {
reporter: ["html", "json"],
include: ["src/**/*.{ts,tsx,js,jsx}"],