mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Compare commits
3 Commits
toger5/bum
...
toger5/con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
121d87b1d0 | ||
|
|
d5b218283d | ||
|
|
d4a3c8bd80 |
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,4 +1,11 @@
|
||||
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
|
||||
<!-- Thanks for submitting a PR! Please read CONTRIBUTING.md before you start. -->
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Features and UI changes require a pre-approved issue.**
|
||||
> Every PR must have a linked issue
|
||||
> that a maintainer has reviewed and approved **before you started writing code**.
|
||||
> PRs that don't meet this requirement will not be reviewed.
|
||||
> See [CONTRIBUTING.md](https://github.com/element-hq/element-call/blob/livekit/CONTRIBUTING.md) for ElementCall decided for this approach.
|
||||
|
||||
## Content
|
||||
|
||||
@@ -6,34 +13,34 @@
|
||||
|
||||
## Motivation and context
|
||||
|
||||
<!-- Provide link to the corresponding issue if applicable or explain the context -->
|
||||
<!-- Provide a link to the pre-approved issue, or explain the context for a bug fix -->
|
||||
|
||||
## Screenshots / GIFs
|
||||
|
||||
<!--
|
||||
|
||||
You can use a table like this to show screenshots comparison.
|
||||
Uncomment this markdown table below and edit the last line `|||`:
|
||||
|copy screenshot of before here|copy screenshot of after here|
|
||||
You can use a table like this to show a before/after comparison.
|
||||
Uncomment the markdown table below and fill in the last line:
|
||||
|
||||
|Before|After|
|
||||
|-|-|
|
||||
|||
|
||||
-->
|
||||
|
||||
-->
|
||||
|
||||
## Tests
|
||||
|
||||
<!-- Explain how you tested your development -->
|
||||
<!-- Explain how you tested your changes -->
|
||||
|
||||
- Step 1
|
||||
- Step 2
|
||||
- Step ...
|
||||
-
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read through [CONTRIBUTING.md](https://github.com/element-hq/element-call/blob/livekit/CONTRIBUTING.md).
|
||||
- [ ] Pull request includes screenshots or videos if containing UI changes
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] A linked, pre-approved issue exists for this feature or UI change.
|
||||
- [ ] I have read [CONTRIBUTING.md](https://github.com/element-hq/element-call/blob/livekit/CONTRIBUTING.md) in full.
|
||||
- [ ] Pull request includes screenshots or videos for any UI changes.
|
||||
- [ ] Tests written for new code (and existing touched code where feasible).
|
||||
- [ ] Linter and other CI checks pass.
|
||||
- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-call)
|
||||
|
||||
28
.github/workflows/build.yaml
vendored
28
.github/workflows/build.yaml
vendored
@@ -85,31 +85,3 @@ jobs:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build_storybook:
|
||||
name: Build Storybook
|
||||
if: contains(github.event.pull_request.labels.*.name, 'storybook build')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: pnpm cache
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: ".node-version"
|
||||
- name: Install dependencies
|
||||
run: "pnpm install --frozen-lockfile --ignore-pnpmfile"
|
||||
- name: Build Storybook
|
||||
run: pnpm run build-storybook
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: build-output-storybook
|
||||
path: storybook-static
|
||||
# We'll only use this in a triggered job, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
10
.github/workflows/deploy-to-netlify.yaml
vendored
10
.github/workflows/deploy-to-netlify.yaml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
package:
|
||||
required: true
|
||||
type: string
|
||||
description: Which package to deploy - 'full', 'embedded', 'sdk', or 'storybook'
|
||||
description: Which package to deploy - 'full', 'embedded', or 'sdk'
|
||||
artifact_run_id:
|
||||
required: false
|
||||
type: string
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
with:
|
||||
step: start
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
env: ${{ inputs.package}}
|
||||
env: Netlify
|
||||
ref: ${{ inputs.deployment_ref }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
@@ -59,13 +59,9 @@ jobs:
|
||||
|
||||
- name: Add redirects file
|
||||
# We fetch from github directly as we don't bother checking out the repo
|
||||
# Not needed for storybook deployments
|
||||
if: inputs.package != 'storybook'
|
||||
run: curl -s https://raw.githubusercontent.com/element-hq/element-call/main/config/netlify_redirects > webapp/_redirects
|
||||
|
||||
- name: Add config file
|
||||
# Not needed for storybook deployments
|
||||
if: inputs.package != 'storybook'
|
||||
run: |
|
||||
if [ "${INPUTS_PACKAGE}" = "full" ]; then
|
||||
curl -s "https://raw.githubusercontent.com/${INPUTS_PR_HEAD_FULL_NAME}/${INPUTS_PR_HEAD_REF}/config/config_netlify_preview.json" > webapp/config.json
|
||||
@@ -82,7 +78,7 @@ jobs:
|
||||
with:
|
||||
publish-dir: webapp
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
alias: ${{ inputs.package == 'sdk' && format('pr{0}-sdk', inputs.pr_number) || inputs.package == 'storybook' && format('pr{0}-storybook', inputs.pr_number) || format('pr{0}', inputs.pr_number) }}
|
||||
alias: ${{ inputs.package == 'sdk' && format('pr{0}-sdk', inputs.pr_number) || format('pr{0}', inputs.pr_number) }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
|
||||
20
.github/workflows/pr-deploy.yaml
vendored
20
.github/workflows/pr-deploy.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
# 2. Event must be a pull_request
|
||||
# 3. Head repository must be the SAME as the base repository (No Forks!)
|
||||
if: >
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.head_repository.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
@@ -63,24 +63,6 @@ jobs:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
|
||||
netlify-storybook:
|
||||
needs: prdetails
|
||||
if: ${{ needs.prdetails.outputs.pr_data_json && contains(fromJSON(needs.prdetails.outputs.pr_data_json).labels.*.name, 'storybook build') }}
|
||||
permissions:
|
||||
deployments: write
|
||||
uses: ./.github/workflows/deploy-to-netlify.yaml
|
||||
with:
|
||||
artifact_run_id: ${{ github.event.workflow_run.id || github.run_id }}
|
||||
pr_number: ${{ needs.prdetails.outputs.pr_number }}
|
||||
pr_head_full_name: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
pr_head_ref: ${{ needs.prdetails.outputs.pr_data_json && fromJSON(needs.prdetails.outputs.pr_data_json).head.ref }}
|
||||
deployment_ref: ${{ needs.prdetails.outputs.pr_data_json && fromJSON(needs.prdetails.outputs.pr_data_json).head.sha || github.ref || github.head_ref }}
|
||||
package: storybook
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
|
||||
docker:
|
||||
if: ${{ needs.prdetails.outputs.pr_data_json && contains(fromJSON(needs.prdetails.outputs.pr_data_json).labels.*.name, 'docker build') }}
|
||||
needs: prdetails
|
||||
|
||||
@@ -1,3 +1,71 @@
|
||||
# Contributing code to Element
|
||||
|
||||
Element follows the same pattern as the [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md).
|
||||
|
||||
# Contributing to Element Call
|
||||
|
||||
Element Call is a native Matrix video conferencing application built on
|
||||
[MatrixRTC (MSC4143)](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
|
||||
and [LiveKit](https://livekit.io/). It runs in multiple deployment contexts — as a
|
||||
standalone web app and as a widget embedded in Element Web, Element X iOS, and
|
||||
Element X Android. It is also the primary R&D foundation for MatrixRTC, which means
|
||||
its architecture, maintainability, and flexibility are held to a high standard.
|
||||
|
||||
We welcome contributions from the community. This document explains how to
|
||||
contribute effectively so that both you and the maintainers get the best outcome.
|
||||
|
||||
## Issue First Policy
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Before writing a single line of code for a new feature or UI change, you **must**
|
||||
> open an issue and have the approach agreed with the maintainers.
|
||||
>
|
||||
> **We will not review or merge feature or UI pull requests that arrive without a
|
||||
> corresponding, pre-approved issue.**
|
||||
|
||||
This is not gatekeeping — it's how we prevent wasted effort on both sides. Element
|
||||
Call must work correctly across multiple deployment contexts and meet specific product
|
||||
and design requirements. It is also a fast-moving codebase that underpins ongoing
|
||||
MatrixRTC development. A PR that looks reasonable in isolation can easily conflict
|
||||
with in-progress work, planned architecture changes, or design decisions that haven't
|
||||
been publicly documented yet.
|
||||
|
||||
The issue is where we resolve all of that **before** anyone writes code.
|
||||
|
||||
**Bug fixes** are no exception — most confirmed bugs should already have an issue anyways, existing issues that are marked as bugs have an implicit maintainer approval. If the solution for the bug is controversial it is highly recommended to discuss the approach in the issue before opening a PR.
|
||||
|
||||
## Contribution Workflow
|
||||
|
||||
1. **Open an issue** using the [Enhancement request](https://github.com/element-hq/element-call/issues/new?template=enhancement.yml) template.
|
||||
2. **Wait for feedback.** A maintainer will comment on the issue **within two weeks**. The use case and approach will get dicussed.
|
||||
This may involve questions, suggestions, or a request to adjust scope.
|
||||
This also allows to bring design and product into the loop before code gets created.
|
||||
3. **Get a green light.** Wait for explicit approval from a maintainer before starting
|
||||
implementation.
|
||||
4. **Implement.** Write the code against the agreed approach.
|
||||
5. **Open a PR.** Link to the issue in your PR description and satisfy the checklist
|
||||
in the PR template.
|
||||
|
||||
## Code Quality
|
||||
|
||||
Element Call moves fast and the codebase must stay clean and maintainable.
|
||||
|
||||
- **Take responsibility for AI-generated code.** AI tools can be a useful aid, but we expect all the generated code to be understood and reasoned about by the contributor. Questions by the maintainers should be answered without just forwarding them to AI. The maintainers also have access to AI tools. If your contribution is just transporting messages between LLM <-> maintaines all our time is better used if the maintainers decide to interact with AI for this specific problem by themselves.
|
||||
- **Think across deployment contexts.** Changes must work correctly in both standalone
|
||||
and widget modes. Consider how your change interacts with Element Web, Element X
|
||||
iOS, and Element X Android.
|
||||
- **Write tests.** New functionality should be covered by tests. Where it is feasible,
|
||||
existing uncovered code touched by your PR should also gain tests.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
All contributors must sign the
|
||||
[Element Contributor License Agreement](https://cla-assistant.io/element-hq/element-call)
|
||||
before their contribution can be merged. The CLA assistant bot will prompt you
|
||||
automatically when you open a PR.
|
||||
|
||||
## Getting Help
|
||||
|
||||
The best place to ask questions about Element Call development is the MatrixRTC room:
|
||||
|
||||
**[#matrixRtc:matrix.org](https://matrix.to/#/#matrixrtc:matrix.org)**
|
||||
|
||||
@@ -42,6 +42,8 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
# Synapse reverse proxy including .well-known/matrix/client
|
||||
@@ -89,6 +91,8 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
# MatrixRTC reverse proxy
|
||||
@@ -140,6 +144,8 @@ server {
|
||||
proxy_pass http://livekit-sfu:7880/;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
# MatrixRTC reverse proxy
|
||||
@@ -186,6 +192,8 @@ server {
|
||||
proxy_pass http://livekit-sfu-1:17880/;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
# Convenience reverse proxy for the call.m.localhost domain to element call
|
||||
@@ -235,6 +243,7 @@ server {
|
||||
proxy_pass http://host.docker.internal:8080;
|
||||
|
||||
}
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
@@ -267,6 +276,8 @@ server {
|
||||
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
# Convenience reverse proxy app.othersite.m.localhost for element web
|
||||
@@ -298,4 +309,6 @@ server {
|
||||
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
}
|
||||
|
||||
@@ -145,7 +145,6 @@
|
||||
},
|
||||
"layout_grid_label": "Grid",
|
||||
"layout_spotlight_label": "Spotlight",
|
||||
"layout_switch_label": "Layout",
|
||||
"lobby": {
|
||||
"ask_to_join": "Request to join call",
|
||||
"join_as_guest": "Join as guest",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.5",
|
||||
"@babel/preset-env": "^7.29.5",
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.23.0",
|
||||
"@codecov/vite-plugin": "^1.3.0",
|
||||
|
||||
@@ -142,7 +142,9 @@ export class TestHelpers {
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
await this.dismissStartupToasts(page);
|
||||
await this.maybeDismissBrowserNotSupportedToast(page);
|
||||
await this.maybeDismissServiceWorkerWarningToast(page);
|
||||
await this.maybeDismissBackupChat(page);
|
||||
|
||||
await TestHelpers.setDevToolElementCallDevUrl(page);
|
||||
|
||||
@@ -153,39 +155,73 @@ export class TestHelpers {
|
||||
return { page, clientHandle, mxId };
|
||||
}
|
||||
|
||||
// Dismisses any toasts that appear on startup, such as "Failed to load service worker" or "Back up your chats".
|
||||
// Toast can be stacked, and only the top one can be dismiss, so just look at what is on top and
|
||||
// dismiss (if part of expected toats)
|
||||
public static async dismissStartupToasts(page: Page): Promise<void> {
|
||||
const expectedToasts = [
|
||||
{ title: "Failed to load service worker", button: "OK" },
|
||||
{ title: "Back up your chats", button: "Dismiss" },
|
||||
{ title: "Element does not support this browser", button: "Dismiss" },
|
||||
];
|
||||
private static async maybeDismissBrowserNotSupportedToast(
|
||||
page: Page,
|
||||
): Promise<void> {
|
||||
const browserUnsupportedToast = page
|
||||
.getByText("Element does not support this browser")
|
||||
.locator("..")
|
||||
.locator("..");
|
||||
|
||||
const toast = page.locator(".mx_Toast_toast");
|
||||
// Dismiss incompatible browser toast
|
||||
const dismissButton = browserUnsupportedToast.getByRole("button", {
|
||||
name: "Dismiss",
|
||||
});
|
||||
try {
|
||||
await expect(dismissButton).toBeVisible({ timeout: 700 });
|
||||
await dismissButton.click();
|
||||
} catch {
|
||||
// dismissButton not visible, continue as normal
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
await toast.waitFor({ state: "visible", timeout: 700 });
|
||||
const title = await toast.locator(".mx_Toast_title h2").textContent();
|
||||
private static async maybeDismissServiceWorkerWarningToast(
|
||||
page: Page,
|
||||
): Promise<void> {
|
||||
const toast = page
|
||||
.locator(".mx_Toast_toast")
|
||||
.getByText("Failed to load service worker");
|
||||
|
||||
// Find the matching toast config
|
||||
const toastConfig = expectedToasts.find((t) =>
|
||||
title?.includes(t.title),
|
||||
);
|
||||
try {
|
||||
await expect(toast).toBeVisible({ timeout: 700 });
|
||||
await page
|
||||
.locator(".mx_Toast_toast")
|
||||
.getByRole("button", { name: "OK" })
|
||||
.click();
|
||||
} catch {
|
||||
// toast not visible, continue as normal
|
||||
}
|
||||
}
|
||||
|
||||
if (toastConfig) {
|
||||
await toast.getByRole("button", { name: toastConfig.button }).click();
|
||||
} else {
|
||||
// Unknown toast. We don't want to act on unknown toasts
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// No toast visible, exit loop
|
||||
break;
|
||||
}
|
||||
private static async maybeDismissBackupChat(page: Page): Promise<void> {
|
||||
const toast = page
|
||||
.locator(".mx_Toast_toast")
|
||||
.getByText("Back up your chats");
|
||||
|
||||
try {
|
||||
await expect(toast).toBeVisible({ timeout: 700 });
|
||||
await page
|
||||
.locator(".mx_Toast_toast")
|
||||
.getByRole("button", { name: "Dismiss" })
|
||||
.click();
|
||||
} catch {
|
||||
// toast not visible, continue as normal
|
||||
}
|
||||
}
|
||||
|
||||
public static async maybeDismissKeyBackupToast(page: Page): Promise<void> {
|
||||
const toast = page
|
||||
.locator(".mx_Toast_toast")
|
||||
.getByText("Back up your chats");
|
||||
|
||||
try {
|
||||
await expect(toast).toBeVisible({ timeout: 700 });
|
||||
await page
|
||||
.locator(".mx_Toast_toast")
|
||||
.getByRole("button", { name: "Dismiss" })
|
||||
.click();
|
||||
} catch {
|
||||
// toast not visible, continue as normal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +244,7 @@ export class TestHelpers {
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByText("Encryption enabled")).toBeVisible();
|
||||
await TestHelpers.dismissStartupToasts(page);
|
||||
await TestHelpers.maybeDismissKeyBackupToast(page);
|
||||
|
||||
// Invite users if any
|
||||
if (andInvite.length > 0) {
|
||||
@@ -247,7 +283,7 @@ export class TestHelpers {
|
||||
await expect(
|
||||
page.getByRole("main").getByRole("heading", { name: roomName }),
|
||||
).toBeVisible();
|
||||
await TestHelpers.dismissStartupToasts(page);
|
||||
await TestHelpers.maybeDismissKeyBackupToast(page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -217,7 +217,7 @@ widgetTest(
|
||||
).toBeVisible();
|
||||
|
||||
await expect(whistler.page.getByText("Incoming video call")).toBeVisible();
|
||||
await whistler.page.getByRole("button", { name: "Decline" }).click();
|
||||
await whistler.page.getByRole("button", { name: "Ignore" }).click();
|
||||
|
||||
await expect(
|
||||
whistler.page.locator('iframe[title="Element Call"]'),
|
||||
|
||||
172
pnpm-lock.yaml
generated
172
pnpm-lock.yaml
generated
@@ -22,8 +22,8 @@ importers:
|
||||
specifier: ^7.16.5
|
||||
version: 7.29.0
|
||||
'@babel/preset-env':
|
||||
specifier: ^7.29.5
|
||||
version: 7.29.5(@babel/core@7.29.0)
|
||||
specifier: ^7.22.20
|
||||
version: 7.29.2(@babel/core@7.29.0)
|
||||
'@babel/preset-react':
|
||||
specifier: ^7.22.15
|
||||
version: 7.28.5(@babel/core@7.29.0)
|
||||
@@ -47,16 +47,16 @@ 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.8(@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.20(livekit-client@2.18.8(@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.8(@types/dom-mediacapture-record@1.0.22))
|
||||
'@mediapipe/tasks-vision':
|
||||
specifier: ^0.10.18
|
||||
version: 0.10.34
|
||||
@@ -227,7 +227,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.8(@types/dom-mediacapture-record@1.0.22)
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.18.1
|
||||
@@ -236,7 +236,7 @@ importers:
|
||||
version: 1.9.2
|
||||
matrix-js-sdk:
|
||||
specifier: matrix-org/matrix-js-sdk#develop
|
||||
version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68
|
||||
version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d
|
||||
matrix-widget-api:
|
||||
specifier: ^1.16.1
|
||||
version: 1.17.0
|
||||
@@ -361,8 +361,8 @@ packages:
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/compat-data@7.29.3':
|
||||
resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==}
|
||||
'@babel/compat-data@7.29.0':
|
||||
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/core@7.29.0':
|
||||
@@ -502,12 +502,6 @@ 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'}
|
||||
@@ -706,8 +700,8 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/plugin-transform-modules-systemjs@7.29.4':
|
||||
resolution: {integrity: sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==}
|
||||
'@babel/plugin-transform-modules-systemjs@7.29.0':
|
||||
resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
@@ -904,8 +898,8 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
|
||||
'@babel/preset-env@7.29.5':
|
||||
resolution: {integrity: sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==}
|
||||
'@babel/preset-env@7.29.2':
|
||||
resolution: {integrity: sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
@@ -1562,12 +1556,12 @@ packages:
|
||||
livekit-client: ^2.17.2
|
||||
tslib: ^2.6.2
|
||||
|
||||
'@livekit/components-react@2.9.21':
|
||||
resolution: {integrity: sha512-6hU9VucJJL+gAhilNGe4MBCDCZVk64qyjP9Ck86krvOIdVU76WeWksddg1MYUP10AlUwwrfD7davz41pJTcMJw==}
|
||||
'@livekit/components-react@2.9.20':
|
||||
resolution: {integrity: sha512-hjkYOsJj9Jbghb7wM5cI8HoVisKeL6Zcy1VnRWTLm0sqVbto8GJp/17T4Udx85mCPY6Jgh8I1Cv0yVzgz7CQtg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@livekit/krisp-noise-filter': ^0.2.12 || ^0.3.0
|
||||
livekit-client: ^2.18.2
|
||||
livekit-client: ^2.17.2
|
||||
react: '>=18'
|
||||
react-dom: '>=18'
|
||||
tslib: ^2.6.2
|
||||
@@ -3001,8 +2995,8 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/project-service@8.59.3':
|
||||
resolution: {integrity: sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==}
|
||||
'@typescript-eslint/project-service@8.59.1':
|
||||
resolution: {integrity: sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
@@ -3019,8 +3013,8 @@ packages:
|
||||
resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/scope-manager@8.59.3':
|
||||
resolution: {integrity: sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==}
|
||||
'@typescript-eslint/scope-manager@8.59.1':
|
||||
resolution: {integrity: sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.58.2':
|
||||
@@ -3035,8 +3029,8 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.59.3':
|
||||
resolution: {integrity: sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==}
|
||||
'@typescript-eslint/tsconfig-utils@8.59.1':
|
||||
resolution: {integrity: sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
@@ -3064,10 +3058,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/types@8.59.3':
|
||||
resolution: {integrity: sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@5.62.0':
|
||||
resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -3089,8 +3079,8 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.59.3':
|
||||
resolution: {integrity: sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==}
|
||||
'@typescript-eslint/typescript-estree@8.59.1':
|
||||
resolution: {integrity: sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
@@ -3115,8 +3105,8 @@ packages:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/utils@8.59.3':
|
||||
resolution: {integrity: sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==}
|
||||
'@typescript-eslint/utils@8.59.1':
|
||||
resolution: {integrity: sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
@@ -3134,13 +3124,12 @@ packages:
|
||||
resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.59.3':
|
||||
resolution: {integrity: sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==}
|
||||
'@typescript-eslint/visitor-keys@8.59.1':
|
||||
resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
'@use-gesture/core@10.3.1':
|
||||
resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==}
|
||||
@@ -4848,6 +4837,9 @@ packages:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
jose@6.2.2:
|
||||
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
|
||||
|
||||
jose@6.2.3:
|
||||
resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
|
||||
|
||||
@@ -5029,8 +5021,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.8:
|
||||
resolution: {integrity: sha512-E+bSpnBVng/1xG4RfL1Q51dHUpBwL14Wix4sR5bS0djEzKMEtrxcUyhWLltdwQ0USf1t0PaxW6WL4oVb2s4Fsw==}
|
||||
peerDependencies:
|
||||
'@types/dom-mediacapture-record': ^1
|
||||
|
||||
@@ -5104,8 +5096,8 @@ packages:
|
||||
matrix-events-sdk@0.0.1:
|
||||
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
||||
|
||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68:
|
||||
resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68}
|
||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d:
|
||||
resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d}
|
||||
version: 41.4.0
|
||||
engines: {node: '>=22.0.0'}
|
||||
|
||||
@@ -6003,11 +5995,6 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.8.0:
|
||||
resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
@@ -6889,7 +6876,7 @@ snapshots:
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/compat-data@7.29.3': {}
|
||||
'@babel/compat-data@7.29.0': {}
|
||||
|
||||
'@babel/core@7.29.0':
|
||||
dependencies:
|
||||
@@ -6939,7 +6926,7 @@ snapshots:
|
||||
|
||||
'@babel/helper-compilation-targets@7.28.6':
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.29.3
|
||||
'@babel/compat-data': 7.29.0
|
||||
'@babel/helper-validator-option': 7.27.1
|
||||
browserslist: 4.28.2
|
||||
lru-cache: 5.1.1
|
||||
@@ -7077,14 +7064,6 @@ 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
|
||||
@@ -7300,7 +7279,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.0)':
|
||||
'@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
|
||||
@@ -7515,9 +7494,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.5(@babel/core@7.29.0)':
|
||||
'@babel/preset-env@7.29.2(@babel/core@7.29.0)':
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.29.3
|
||||
'@babel/compat-data': 7.29.0
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-compilation-targets': 7.28.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
@@ -7525,7 +7504,6 @@ 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)
|
||||
@@ -7557,7 +7535,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.4(@babel/core@7.29.0)
|
||||
'@babel/plugin-transform-modules-systemjs': 7.29.0(@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)
|
||||
@@ -8226,21 +8204,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.8(@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.8(@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.20(livekit-client@2.18.8(@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.8(@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)
|
||||
jose: 6.2.2
|
||||
livekit-client: 2.18.8(@types/dom-mediacapture-record@1.0.22)
|
||||
react: 19.2.5
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
tslib: 2.8.1
|
||||
@@ -8256,11 +8234,11 @@ snapshots:
|
||||
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/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.8(@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.8(@types/dom-mediacapture-record@1.0.22)
|
||||
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.2.0': {}
|
||||
|
||||
@@ -9523,10 +9501,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.59.3(typescript@5.9.3)':
|
||||
'@typescript-eslint/project-service@8.59.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.59.3
|
||||
'@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.59.1
|
||||
debug: 4.4.3
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -9547,10 +9525,10 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.59.0
|
||||
'@typescript-eslint/visitor-keys': 8.59.0
|
||||
|
||||
'@typescript-eslint/scope-manager@8.59.3':
|
||||
'@typescript-eslint/scope-manager@8.59.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.59.3
|
||||
'@typescript-eslint/visitor-keys': 8.59.3
|
||||
'@typescript-eslint/types': 8.59.1
|
||||
'@typescript-eslint/visitor-keys': 8.59.1
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)':
|
||||
dependencies:
|
||||
@@ -9560,7 +9538,7 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.59.3(typescript@5.9.3)':
|
||||
'@typescript-eslint/tsconfig-utils@8.59.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
@@ -9584,8 +9562,6 @@ snapshots:
|
||||
|
||||
'@typescript-eslint/types@8.59.1': {}
|
||||
|
||||
'@typescript-eslint/types@8.59.3': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.62.0
|
||||
@@ -9630,15 +9606,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.59.3(typescript@5.9.3)':
|
||||
'@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.59.3(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.59.3
|
||||
'@typescript-eslint/visitor-keys': 8.59.3
|
||||
'@typescript-eslint/project-service': 8.59.1(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.59.1
|
||||
'@typescript-eslint/visitor-keys': 8.59.1
|
||||
debug: 4.4.3
|
||||
minimatch: 10.2.5
|
||||
semver: 7.8.0
|
||||
semver: 7.7.4
|
||||
tinyglobby: 0.2.16
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
@@ -9682,12 +9658,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.59.3(eslint@8.57.1)(typescript@5.9.3)':
|
||||
'@typescript-eslint/utils@8.59.1(eslint@8.57.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
|
||||
'@typescript-eslint/scope-manager': 8.59.3
|
||||
'@typescript-eslint/types': 8.59.3
|
||||
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.59.1
|
||||
'@typescript-eslint/types': 8.59.1
|
||||
'@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -9708,9 +9684,9 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.59.0
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.59.3':
|
||||
'@typescript-eslint/visitor-keys@8.59.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.59.3
|
||||
'@typescript-eslint/types': 8.59.1
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
@@ -10015,7 +9991,7 @@ snapshots:
|
||||
|
||||
babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0):
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.29.3
|
||||
'@babel/compat-data': 7.29.0
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0)
|
||||
semver: 6.3.1
|
||||
@@ -10871,7 +10847,7 @@ snapshots:
|
||||
|
||||
eslint-plugin-jest@29.15.2(@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.59.3(eslint@8.57.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.59.1(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
|
||||
@@ -11760,6 +11736,8 @@ snapshots:
|
||||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jose@6.2.2: {}
|
||||
|
||||
jose@6.2.3: {}
|
||||
|
||||
js-tokens@10.0.0: {}
|
||||
@@ -11929,7 +11907,7 @@ snapshots:
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22):
|
||||
livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22):
|
||||
dependencies:
|
||||
'@livekit/mutex': 1.1.1
|
||||
'@livekit/protocol': 1.45.3
|
||||
@@ -12005,7 +11983,7 @@ snapshots:
|
||||
|
||||
matrix-events-sdk@0.0.1: {}
|
||||
|
||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68:
|
||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@matrix-org/matrix-sdk-crypto-wasm': 18.2.0
|
||||
@@ -13082,8 +13060,6 @@ snapshots:
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
semver@7.8.0: {}
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
set-cookie-parser@2.7.2: {}
|
||||
|
||||
@@ -88,24 +88,6 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAudioAndVideoOptions: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
audioEnabled: false,
|
||||
videoEnabled: true,
|
||||
audioOptions: [
|
||||
{ label: "Microphone 1", id: "1" },
|
||||
{ label: "Microphone 2", id: "2" },
|
||||
],
|
||||
videoOptions: [
|
||||
{ label: "Camera 1", id: "1" },
|
||||
{ label: "Camera 2", id: "2" },
|
||||
],
|
||||
selectedAudio: "2",
|
||||
selectedVideo: "1",
|
||||
},
|
||||
};
|
||||
export const WithLogo: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
|
||||
@@ -8,12 +8,6 @@ Please see LICENSE in the repository root for full details.
|
||||
import { type FC, type JSX, type Ref, useMemo } from "react";
|
||||
import classNames from "classnames";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Switch } from "@vector-im/compound-web";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
SpotlightIcon,
|
||||
GridIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import LogoMark from "../icons/LogoMark.svg?react";
|
||||
import LogoType from "../icons/LogoType.svg?react";
|
||||
@@ -29,11 +23,8 @@ import {
|
||||
type ReactionData,
|
||||
} from "../button";
|
||||
import styles from "./CallFooter.module.css";
|
||||
import { LayoutToggle } from "../room/LayoutToggle";
|
||||
import { type GridMode } from "../state/CallViewModel/CallViewModel";
|
||||
import {
|
||||
MediaMuteAndSwitchButton,
|
||||
type MenuOptions,
|
||||
} from "./MediaMuteAndSwitchButton";
|
||||
|
||||
export interface AudioOutputSwitcher {
|
||||
targetOutput: string;
|
||||
@@ -83,13 +74,6 @@ export interface FooterProps {
|
||||
// debug stuff
|
||||
debugTileLayout?: boolean;
|
||||
tileStoreGeneration?: number;
|
||||
|
||||
audioOptions?: MenuOptions[];
|
||||
videoOptions?: MenuOptions[];
|
||||
selectedAudio?: string;
|
||||
selectedVideo?: string;
|
||||
selectAudioDevice?: (deviceId: string) => void;
|
||||
selectVideoDevice?: (deviceId: string) => void;
|
||||
}
|
||||
|
||||
export const CallFooter: FC<FooterProps> = ({
|
||||
@@ -115,13 +99,6 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
hangup,
|
||||
debugTileLayout,
|
||||
tileStoreGeneration,
|
||||
|
||||
audioOptions,
|
||||
videoOptions,
|
||||
selectedAudio,
|
||||
selectedVideo,
|
||||
selectAudioDevice,
|
||||
selectVideoDevice,
|
||||
}) => {
|
||||
const buttons: JSX.Element[] = [];
|
||||
const buttonSize = asPip ? "md" : "lg";
|
||||
@@ -143,58 +120,24 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if ((audioOptions?.length ?? 0) > 0) {
|
||||
buttons.push(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Mic Source"}
|
||||
key="audio"
|
||||
iconsAndLabels="audio"
|
||||
enabled={audioEnabled ?? false}
|
||||
onMuteClick={toggleAudio}
|
||||
data-testid="incall_mute"
|
||||
options={audioOptions}
|
||||
selectedOption={selectedAudio}
|
||||
onSelect={selectAudioDevice}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
<MicButton
|
||||
size={buttonSize}
|
||||
key="audio"
|
||||
enabled={audioEnabled ?? false}
|
||||
onClick={toggleAudio}
|
||||
disabled={toggleAudio === undefined}
|
||||
data-testid="incall_mute"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
if ((videoOptions?.length ?? 0) > 0) {
|
||||
buttons.push(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Camera Source"}
|
||||
key="video"
|
||||
iconsAndLabels="video"
|
||||
enabled={videoEnabled ?? false}
|
||||
onMuteClick={toggleVideo}
|
||||
data-testid="incall_videomute"
|
||||
options={videoOptions}
|
||||
selectedOption={selectedVideo}
|
||||
onSelect={selectVideoDevice}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
<VideoButton
|
||||
size={buttonSize}
|
||||
key="video"
|
||||
enabled={videoEnabled ?? false}
|
||||
onClick={toggleVideo}
|
||||
disabled={toggleVideo === undefined}
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
buttons.push(
|
||||
<MicButton
|
||||
size={buttonSize}
|
||||
key="audio"
|
||||
enabled={audioEnabled ?? false}
|
||||
onClick={toggleAudio}
|
||||
disabled={toggleAudio === undefined}
|
||||
data-testid="incall_mute"
|
||||
/>,
|
||||
<VideoButton
|
||||
size={buttonSize}
|
||||
key="video"
|
||||
enabled={videoEnabled ?? false}
|
||||
onClick={toggleVideo}
|
||||
disabled={toggleVideo === undefined}
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
);
|
||||
|
||||
if (toggleScreenSharing !== undefined) {
|
||||
buttons.push(
|
||||
@@ -289,18 +232,10 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
</div>
|
||||
{!hideControls && <div className={styles.buttons}>{buttons}</div>}
|
||||
{setLayoutMode && layoutMode && showLayoutSwitcher && (
|
||||
<Switch<"spotlight", "grid">
|
||||
name="layoutMode"
|
||||
aria-label={t("layout_switch_label")}
|
||||
leftLabel={t("layout_spotlight_label")}
|
||||
leftValue="spotlight"
|
||||
leftIcon={SpotlightIcon}
|
||||
rightLabel={t("layout_grid_label")}
|
||||
rightValue="grid"
|
||||
rightIcon={GridIcon}
|
||||
<LayoutToggle
|
||||
className={styles.layout}
|
||||
value={layoutMode}
|
||||
onChange={setLayoutMode}
|
||||
layout={layoutMode}
|
||||
setLayout={setLayoutMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
border-radius: 32px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
.containerOpen {
|
||||
background-color: var(--cpd-color-bg-action-primary-pressed);
|
||||
}
|
||||
.chevronIconOpen > svg {
|
||||
color: var(--cpd-color-icon-on-solid-primary);
|
||||
}
|
||||
.menuButton {
|
||||
width: 40px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.itemIcon {
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
.rotate {
|
||||
animation: spinner 1.5s linear infinite;
|
||||
}
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
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 { AdvancedSettingsIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { fn, userEvent, within, expect } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton";
|
||||
|
||||
const meta = {
|
||||
component: MediaMuteAndSwitchButton,
|
||||
} satisfies Meta<typeof MediaMuteAndSwitchButton>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: "SomeMenu",
|
||||
iconsAndLabels: {
|
||||
IconEnabled: AdvancedSettingsIcon,
|
||||
IconDisabled: AdvancedSettingsIcon,
|
||||
enabledLabel: "Enabled",
|
||||
disabledLabel: "Disabled",
|
||||
optionsButtonLabel: "Options",
|
||||
},
|
||||
enabled: true,
|
||||
options: [
|
||||
{ label: "option 1", id: "1" },
|
||||
{ label: "option 2", id: "2" },
|
||||
],
|
||||
selectedOption: "1",
|
||||
onMuteClick: fn(),
|
||||
onSelect: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export const AudioMute: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
title: "Microphone",
|
||||
iconsAndLabels: "audio",
|
||||
enabled: false,
|
||||
options: [
|
||||
{ label: "Microphone 1", id: "1" },
|
||||
{ label: "Microphone 2", id: "2" },
|
||||
],
|
||||
toggles: [
|
||||
{
|
||||
label: "example toggle",
|
||||
id: "t0",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
selectedOption: "2",
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
// Both the mute button and the chevron trigger currently share the aria-label "Edit"
|
||||
// (both are TODO placeholders in the component). The mute button is first in the DOM.
|
||||
const muteButton = canvas.getByLabelText("Unmute microphone");
|
||||
await userEvent.click(muteButton);
|
||||
await expect(args.onMuteClick).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const AudioUnmute: Story = {
|
||||
args: {
|
||||
title: "Microphone",
|
||||
iconsAndLabels: "audio",
|
||||
enabled: true,
|
||||
options: [
|
||||
{ label: "Microphone 1", id: "1" },
|
||||
{ label: "Microphone 2", id: "2" },
|
||||
],
|
||||
toggles: [],
|
||||
selectedOption: "2",
|
||||
},
|
||||
};
|
||||
|
||||
export const VideoMute: Story = {
|
||||
args: {
|
||||
title: "Camera",
|
||||
iconsAndLabels: "video",
|
||||
enabled: false,
|
||||
options: [
|
||||
{ label: "Camera 1", id: "1" },
|
||||
{ label: "Camera 2", id: "2" },
|
||||
],
|
||||
toggles: [],
|
||||
selectedOption: "1",
|
||||
},
|
||||
};
|
||||
|
||||
export const VideoUnmute: Story = {
|
||||
args: {
|
||||
title: "Camera",
|
||||
iconsAndLabels: "video",
|
||||
enabled: true,
|
||||
options: [
|
||||
{ label: "Camera 1", id: "1" },
|
||||
{ label: "Camera 2", id: "2" },
|
||||
],
|
||||
toggles: [
|
||||
{
|
||||
label: "Blur Background",
|
||||
id: "background_blurring",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
selectedOption: "2",
|
||||
},
|
||||
};
|
||||
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
Copyright 2023, 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { act, render, screen, type RenderResult } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type JSX, useState } from "react";
|
||||
|
||||
import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton";
|
||||
|
||||
describe("MediaMuteAndSwitchButton", () => {
|
||||
test("renders", () => {
|
||||
const { container } = render(
|
||||
<MediaMuteAndSwitchButton title={"Switcher"} />,
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("renders correct audio and video labels", () => {
|
||||
const renderLabels = (
|
||||
type: "video" | "audio",
|
||||
enabled: boolean,
|
||||
): RenderResult => {
|
||||
return render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Switcher"}
|
||||
iconsAndLabels={type}
|
||||
enabled={enabled}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
const renderAudioEndabled = renderLabels("audio", true);
|
||||
const renderAudioDisabled = renderLabels("audio", false);
|
||||
const renderVideoEnabled = renderLabels("video", true);
|
||||
const renderVideoDisabled = renderLabels("video", false);
|
||||
|
||||
expect(
|
||||
renderAudioEndabled.getByRole("button", { name: "Mute microphone" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
renderAudioDisabled.getByRole("button", { name: "Unmute microphone" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
renderVideoEnabled.getByRole("button", { name: "Start video" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
renderVideoDisabled.getByRole("button", { name: "Stop video" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls mute on mute press", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onMute = vi.fn();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Switcher"}
|
||||
onMuteClick={onMute}
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Mute microphone" }));
|
||||
|
||||
expect(onMute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls select callback on menu click", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelect = vi.fn();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
options={[
|
||||
{ label: "Microphone 1", id: "mic1" },
|
||||
{ label: "Microphone 2", id: "mic2" },
|
||||
]}
|
||||
selectedOption="mic1"
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 2" }));
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith("mic2");
|
||||
});
|
||||
test("does not call select callback on already selected menu click", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelect = vi.fn();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
options={[
|
||||
{ label: "Microphone 1", id: "mic1" },
|
||||
{ label: "Microphone 2", id: "mic2" },
|
||||
]}
|
||||
selectedOption="mic1"
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 1" }));
|
||||
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders menu spinner until selection updates for the component", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
const onSelectPressed = vi.fn();
|
||||
const onOptionUpdated = vi.fn();
|
||||
function Wrapper(): JSX.Element {
|
||||
const [selectedOption, setSelectedOption] = useState("mic1");
|
||||
return (
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
options={[
|
||||
{ label: "Microphone 1", id: "mic1" },
|
||||
{ label: "Microphone 2", id: "mic2" },
|
||||
]}
|
||||
selectedOption={selectedOption}
|
||||
onSelect={(id) => {
|
||||
onSelectPressed();
|
||||
void promise.then(() => {
|
||||
setSelectedOption(id);
|
||||
onOptionUpdated();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { getByRole } = render(<Wrapper />);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 2" }));
|
||||
|
||||
expect(onSelectPressed).toHaveBeenCalled();
|
||||
expect(onOptionUpdated).not.toHaveBeenCalled();
|
||||
// After clicking, plannedSelection="mic2" but selectedOption is still "mic1",
|
||||
// so a spinner should appear on the mic2 item
|
||||
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
expect(mic2Item.querySelector(".rotate")).toBeTruthy();
|
||||
|
||||
// The currently-selected mic1 item should not have a spinner
|
||||
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
expect(mic1Item.querySelector(".rotate")).toBeNull();
|
||||
await act(async () => {
|
||||
// resolve the promise that acutally updates the select option.
|
||||
resolve();
|
||||
await promise;
|
||||
});
|
||||
|
||||
expect(onOptionUpdated).toHaveBeenCalled();
|
||||
// Spinner should now be gone since the selection has caught up
|
||||
const mic2ItemAfter = screen.getByRole("menuitem", {
|
||||
name: "Microphone 2",
|
||||
});
|
||||
expect(mic2ItemAfter.querySelector(".rotate")).toBeNull();
|
||||
});
|
||||
|
||||
test("renders menu with toggle control and calls toggle callback", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelect = vi.fn();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
toggles={[{ label: "Background blur", id: "bg_blur", enabled: false }]}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
|
||||
const toggle = screen.getByRole("menuitemcheckbox", {
|
||||
name: "Background blur",
|
||||
});
|
||||
expect(toggle).toBeInTheDocument();
|
||||
expect(toggle).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
await user.click(toggle);
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith("bg_blur");
|
||||
});
|
||||
|
||||
test("renders check icon to mark the selected menu item", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
options={[
|
||||
{ label: "Microphone 1", id: "mic1" },
|
||||
{ label: "Microphone 2", id: "mic2" },
|
||||
]}
|
||||
selectedOption="mic2"
|
||||
/>,
|
||||
);
|
||||
|
||||
// open menu
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
|
||||
// The selected item (mic2) renders both an IconOptions SVG and a CheckIcon SVG
|
||||
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
expect(mic1Item.querySelectorAll("svg").length).toBe(2);
|
||||
|
||||
// The unselected item (mic1) only renders its IconOptions SVG
|
||||
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
expect(mic2Item.querySelectorAll("svg").length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,230 +0,0 @@
|
||||
/*
|
||||
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 ComponentType, useState, type FC } from "react";
|
||||
import {
|
||||
Button,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ToggleMenuItem,
|
||||
} from "@vector-im/compound-web";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronUpIcon,
|
||||
ChevronDownIcon,
|
||||
MicOffSolidIcon,
|
||||
MicOnIcon,
|
||||
MicOnSolidIcon,
|
||||
SpinnerIcon,
|
||||
VideoCallIcon,
|
||||
VideoCallOffSolidIcon,
|
||||
VideoCallSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import classNames from "classnames";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import styles from "./MediaMuteAndSwitchButton.module.css";
|
||||
|
||||
export interface MenuOptions {
|
||||
label: string;
|
||||
id: string;
|
||||
}
|
||||
export interface ToggleOption {
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface IconsAndLabels {
|
||||
/** The Icon used if the mute button is enabled */
|
||||
IconEnabled: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||
/** The Icon used if the mute button is disabled */
|
||||
IconDisabled: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||
/** The icon used for the different options */
|
||||
IconOptions?: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||
enabledLabel: string;
|
||||
disabledLabel: string;
|
||||
optionsButtonLabel: string;
|
||||
}
|
||||
|
||||
export interface MediaMuteAndSwitchButtonProps {
|
||||
/** The title used in the Switcher modal. */
|
||||
title: string;
|
||||
/** If the Mute button is enabled */
|
||||
enabled?: boolean;
|
||||
/** Callback if the mute button is clicked */
|
||||
onMuteClick?: () => void;
|
||||
iconsAndLabels?: "video" | "audio" | IconsAndLabels;
|
||||
/** The options available for the media device selector modal */
|
||||
options?: MenuOptions[];
|
||||
/** The option that will currently be rendered as the selected option */
|
||||
selectedOption?: string;
|
||||
/**
|
||||
* The available toggles (including there current state)
|
||||
* The toggle state is not stored by this component.
|
||||
* It is handled externally and needs to be set by listening to the `onSelect` callback and setting the right toggle item to `enabled`
|
||||
*/
|
||||
toggles?: ToggleOption[];
|
||||
/**
|
||||
* For any toggle and option this method will be called.
|
||||
* So toggles need to be implemented by listening here and setting the right toggle item to `enabled`
|
||||
*/
|
||||
onSelect?: (id: string) => void;
|
||||
}
|
||||
|
||||
export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
title,
|
||||
enabled,
|
||||
onMuteClick,
|
||||
iconsAndLabels: iconsAndLabelsWithDefaultCases,
|
||||
options,
|
||||
selectedOption,
|
||||
toggles,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [plannedSelection, setPlannedSelection] = useState<string | null>(null);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
let iconsAndLabels: IconsAndLabels | undefined;
|
||||
switch (iconsAndLabelsWithDefaultCases) {
|
||||
case "video":
|
||||
iconsAndLabels = {
|
||||
IconEnabled: VideoCallSolidIcon,
|
||||
IconDisabled: VideoCallOffSolidIcon,
|
||||
IconOptions: VideoCallIcon,
|
||||
disabledLabel: t("stop_video_button_label"),
|
||||
enabledLabel: t("start_video_button_label"),
|
||||
optionsButtonLabel: t("settings.devices.microphone"),
|
||||
};
|
||||
break;
|
||||
case "audio":
|
||||
iconsAndLabels = {
|
||||
IconEnabled: MicOnSolidIcon,
|
||||
IconDisabled: MicOffSolidIcon,
|
||||
IconOptions: MicOnIcon,
|
||||
disabledLabel: t("mute_microphone_button_label"),
|
||||
enabledLabel: t("unmute_microphone_button_label"),
|
||||
optionsButtonLabel: t("settings.devices.microphone"),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
iconsAndLabels = iconsAndLabelsWithDefaultCases;
|
||||
break;
|
||||
}
|
||||
const {
|
||||
IconEnabled,
|
||||
IconDisabled,
|
||||
IconOptions,
|
||||
disabledLabel,
|
||||
enabledLabel,
|
||||
optionsButtonLabel,
|
||||
} = iconsAndLabels ?? {
|
||||
IconEnabled: undefined,
|
||||
IconDisabled: undefined,
|
||||
IconOptions: undefined,
|
||||
disabledLabel: undefined,
|
||||
enabledLabel: undefined,
|
||||
optionsButtonLabel: undefined,
|
||||
};
|
||||
{
|
||||
logger.info(
|
||||
"RENDER WITH: selectedOption !== option.id && plannedSelection === option.id",
|
||||
selectedOption,
|
||||
" !==",
|
||||
"option.id",
|
||||
" && ",
|
||||
plannedSelection,
|
||||
" === ",
|
||||
"option.id",
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
[styles.container]: true,
|
||||
[styles.containerOpen]: menuOpen,
|
||||
})}
|
||||
>
|
||||
{/* The mute button lives inside */}
|
||||
<Button
|
||||
iconOnly
|
||||
Icon={enabled ? IconEnabled : IconDisabled}
|
||||
onClick={(e) => {
|
||||
onMuteClick?.();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
kind={enabled ? "secondary" : "primary"}
|
||||
size="lg"
|
||||
className={styles.button}
|
||||
aria-label={enabled ? disabledLabel : enabledLabel}
|
||||
/>
|
||||
<Menu
|
||||
title={title}
|
||||
showTitle={true}
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
side="top"
|
||||
trigger={
|
||||
<Button
|
||||
iconOnly
|
||||
className={classNames({
|
||||
[styles.menuButton]: true,
|
||||
[styles.chevronIconOpen]: menuOpen,
|
||||
})}
|
||||
Icon={menuOpen ? ChevronUpIcon : ChevronDownIcon}
|
||||
kind={"tertiary"}
|
||||
size="lg"
|
||||
aria-label={optionsButtonLabel}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{options?.map((option) => (
|
||||
<MenuItem
|
||||
hideChevron
|
||||
label={option.label}
|
||||
Icon={
|
||||
IconOptions && (
|
||||
<IconOptions
|
||||
width={24}
|
||||
height={24}
|
||||
className={styles.itemIcon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
if (option.id === selectedOption) return;
|
||||
setPlannedSelection(option.id);
|
||||
onSelect?.(option.id);
|
||||
}}
|
||||
key={option.id}
|
||||
>
|
||||
{selectedOption === option.id && (
|
||||
<CheckIcon width={24} height={24} />
|
||||
)}
|
||||
{selectedOption !== option.id && plannedSelection === option.id && (
|
||||
<SpinnerIcon width={24} height={24} className={styles.rotate} />
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
{(toggles?.length ?? 0) > 0 && <hr />}
|
||||
{toggles?.map((toggle) => (
|
||||
<ToggleMenuItem
|
||||
label={toggle.label}
|
||||
onSelect={(e) => {
|
||||
onSelect?.(toggle.id);
|
||||
e.preventDefault();
|
||||
}}
|
||||
checked={toggle.enabled}
|
||||
key={toggle.id}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<button
|
||||
class="_button_1nw83_8 button _icon-only_1nw83_53"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
class="_button_1nw83_8 menuButton _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
data-state="closed"
|
||||
id="radix-_r_0_"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -164,14 +164,6 @@ export interface ResolvedConfigOptions extends ConfigOptions {
|
||||
};
|
||||
sync_disconnect_grace_period_ms: number;
|
||||
ssla: string;
|
||||
matrix_rtc_session: {
|
||||
wait_for_key_rotation_ms?: number;
|
||||
delayed_leave_event_delay_ms: number;
|
||||
delayed_leave_event_restart_local_timeout_ms?: number;
|
||||
delayed_leave_event_restart_ms?: number;
|
||||
network_error_retry_ms: number;
|
||||
membership_event_expiry_ms?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
||||
@@ -186,8 +178,4 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
||||
},
|
||||
sync_disconnect_grace_period_ms: 10000,
|
||||
ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||
matrix_rtc_session: {
|
||||
delayed_leave_event_delay_ms: 10000,
|
||||
network_error_retry_ms: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -84,99 +84,6 @@ describe("getSFUConfigWithOpenID", () => {
|
||||
expect.fail("Expected test to throw;");
|
||||
});
|
||||
|
||||
it("should retry without delay params if the JWT service legacy endpoint returns M_BAD_JSON 400", async () => {
|
||||
let callCount = 0;
|
||||
|
||||
fetchMock.post(
|
||||
"https://sfu.example.org/sfu/get",
|
||||
(url, opts) => {
|
||||
callCount++;
|
||||
const body = JSON.parse(opts.body as string);
|
||||
|
||||
// First call: check if it has delay parts and return 400
|
||||
if (callCount === 1) {
|
||||
expect(body).toHaveProperty("delay_id", "mock_delay_id");
|
||||
return {
|
||||
status: 400,
|
||||
body: { errcode: "M_BAD_JSON", error: "Unsupported parameters" },
|
||||
};
|
||||
}
|
||||
|
||||
// Second call: check if delay parts were stripped and return success
|
||||
expect(body).not.toHaveProperty("delay_id");
|
||||
expect(body).not.toHaveProperty("delay_timeout");
|
||||
expect(body).not.toHaveProperty("delay_cs_api_url");
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: { url: sfuUrl, jwt: testJWTToken },
|
||||
};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
// Note: Assuming getSFUConfigWithOpenID eventually calls getLiveKitJWT
|
||||
const config = await getSFUConfigWithOpenID(
|
||||
matrixClient,
|
||||
ownMemberMock,
|
||||
"https://sfu.example.org",
|
||||
"!example_room_id",
|
||||
{
|
||||
delayEndpointBaseUrl: "https://matrix.homeserverserver.org",
|
||||
delayId: "mock_delay_id",
|
||||
},
|
||||
);
|
||||
|
||||
expect(config.jwt).toBe(testJWTToken);
|
||||
expect(callCount).toBe(2);
|
||||
void (await fetchMock.flush());
|
||||
});
|
||||
|
||||
it("should successfully send delay parameters to the JWT service legacy endpoint", async () => {
|
||||
fetchMock.post(
|
||||
"https://sfu.example.org/sfu/get",
|
||||
(url, opts) => {
|
||||
const body = JSON.parse(opts.body as string);
|
||||
|
||||
// Verify, that the request contains the expected delay parameters
|
||||
if (
|
||||
body.delay_id === "mock_delay_id" &&
|
||||
body.delay_timeout === 10000 &&
|
||||
body.delay_cs_api_url === "https://homeserverserver.org/cs_api"
|
||||
) {
|
||||
return {
|
||||
status: 200,
|
||||
body: { url: sfuUrl, jwt: testJWTToken },
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: "Missing expected delay params" },
|
||||
};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
const config = await getSFUConfigWithOpenID(
|
||||
matrixClient,
|
||||
ownMemberMock,
|
||||
"https://sfu.example.org",
|
||||
"!example_room_id",
|
||||
{
|
||||
delayEndpointBaseUrl: "https://homeserverserver.org/cs_api",
|
||||
delayId: "mock_delay_id",
|
||||
},
|
||||
);
|
||||
|
||||
// Prüfe das Ergebnis
|
||||
expect(config).toMatchObject({
|
||||
jwt: testJWTToken,
|
||||
url: sfuUrl,
|
||||
});
|
||||
|
||||
void (await fetchMock.flush());
|
||||
});
|
||||
|
||||
it("should try legacy and then new endpoint with delay delegation", async () => {
|
||||
fetchMock.post("https://sfu.example.org/get_token", () => {
|
||||
return {
|
||||
@@ -214,7 +121,7 @@ describe("getSFUConfigWithOpenID", () => {
|
||||
expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token");
|
||||
expect(calls[0][1]).toStrictEqual({
|
||||
// check if it uses correct delayID!
|
||||
body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":10000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
||||
body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":1000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -224,7 +131,7 @@ describe("getSFUConfigWithOpenID", () => {
|
||||
expect(calls[1][0]).toStrictEqual("https://sfu.example.org/sfu/get");
|
||||
|
||||
expect(calls[1][1]).toStrictEqual({
|
||||
body: '{"room":"!example_room_id","device_id":"DEVICE","delay_id":"mock_delay_id","delay_timeout":10000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
||||
body: '{"room":"!example_room_id","device_id":"DEVICE"}',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -269,7 +176,7 @@ describe("getSFUConfigWithOpenID", () => {
|
||||
expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token");
|
||||
expect(calls[0][1]).toStrictEqual({
|
||||
// check if it uses correct delayID!
|
||||
body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":10000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
||||
body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":1000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -155,8 +155,6 @@ export async function getSFUConfigWithOpenID(
|
||||
serviceUrl,
|
||||
roomId,
|
||||
openIdToken,
|
||||
opts?.delayEndpointBaseUrl,
|
||||
opts?.delayId,
|
||||
);
|
||||
logger?.info(`Got JWT from call's active focus URL.`);
|
||||
return extractFullConfigFromToken(sfuConfig);
|
||||
@@ -189,62 +187,20 @@ async function getLiveKitJWT(
|
||||
livekitServiceURL: string,
|
||||
matrixRoomId: string,
|
||||
openIDToken: IOpenIDToken,
|
||||
delayEndpointBaseUrl?: string,
|
||||
delayId?: string,
|
||||
): Promise<{ url: string; jwt: string }> {
|
||||
interface IDelayParams {
|
||||
delay_id?: string;
|
||||
delay_timeout?: number;
|
||||
delay_cs_api_url?: string;
|
||||
}
|
||||
let bodyDalayParts: IDelayParams = {};
|
||||
// Also check for empty string
|
||||
if (delayId && delayEndpointBaseUrl) {
|
||||
const delayTimeoutMs =
|
||||
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms;
|
||||
bodyDalayParts = {
|
||||
delay_id: delayId,
|
||||
delay_timeout: delayTimeoutMs,
|
||||
delay_cs_api_url: delayEndpointBaseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const makeRequest = async (delayParts: IDelayParams): Promise<Response> => {
|
||||
const res = await doNetworkOperationWithRetry(async () => {
|
||||
return await fetch(livekitServiceURL + "/sfu/get", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// The legacy JWT endpoint uses only the matrix room id to calculate the livekit room alias.
|
||||
// However, the livekit room alias is provided as part of the JWT payload.
|
||||
// This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used.
|
||||
room: matrixRoomId,
|
||||
openid_token: openIDToken,
|
||||
device_id: deviceId,
|
||||
...delayParts,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const res = await doNetworkOperationWithRetry(async () => {
|
||||
let response = await makeRequest(bodyDalayParts);
|
||||
|
||||
// Old service compatibility check
|
||||
const oldServiceDoesNotSupportDelayParts =
|
||||
response.status === 400 && Object.keys(bodyDalayParts).length > 0;
|
||||
// If http status 400 with M_BAD_JSON and we sent delay parts, retry without them
|
||||
if (oldServiceDoesNotSupportDelayParts) {
|
||||
try {
|
||||
const errorBody = await response.json();
|
||||
if (errorBody.errcode === "M_BAD_JSON") {
|
||||
response = await makeRequest({});
|
||||
}
|
||||
} catch {
|
||||
// If we can't parse the error, treat as real error
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -285,7 +241,7 @@ export async function getLiveKitJWTWithDelayDelegation(
|
||||
// Also check for empty string
|
||||
if (delayId && delayEndpointBaseUrl) {
|
||||
const delayTimeoutMs =
|
||||
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms;
|
||||
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms ?? 1000;
|
||||
bodyDalayParts = {
|
||||
delay_id: delayId,
|
||||
delay_timeout: delayTimeoutMs,
|
||||
|
||||
79
src/room/LayoutToggle.module.css
Normal file
79
src/room/LayoutToggle.module.css
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2023, 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
.toggle {
|
||||
padding: 2px;
|
||||
border: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||
border-radius: var(--cpd-radius-pill-effect);
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle input {
|
||||
appearance: none;
|
||||
/* Safari puts a margin on these, which is not removed via appearance: none */
|
||||
margin: 0;
|
||||
block-size: var(--cpd-space-11x);
|
||||
inline-size: var(--cpd-space-11x);
|
||||
cursor: pointer;
|
||||
border-radius: var(--cpd-radius-pill-effect);
|
||||
background: var(--cpd-color-bg-action-secondary-rest);
|
||||
box-shadow: var(--small-drop-shadow);
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
.toggle svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
padding: calc(2.5 * var(--cpd-space-1x));
|
||||
pointer-events: none;
|
||||
color: var(--cpd-color-icon-primary);
|
||||
transition: color 0.1s;
|
||||
}
|
||||
|
||||
.toggle svg:nth-child(2) {
|
||||
inset-inline-start: 2px;
|
||||
}
|
||||
|
||||
.toggle svg:nth-child(4) {
|
||||
inset-inline-end: 2px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.toggle input:hover {
|
||||
background: var(--cpd-color-bg-action-secondary-hovered);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle input:active {
|
||||
background: var(--cpd-color-bg-action-secondary-pressed);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.toggle input:checked {
|
||||
background: var(--cpd-color-bg-action-primary-rest);
|
||||
}
|
||||
|
||||
.toggle input:checked + svg {
|
||||
color: var(--cpd-color-icon-on-solid-primary);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.toggle input:checked:hover {
|
||||
background: var(--cpd-color-bg-action-primary-hovered);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle input:checked:active {
|
||||
background: var(--cpd-color-bg-action-primary-pressed);
|
||||
}
|
||||
|
||||
.toggle input:first-child {
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
25
src/room/LayoutToggle.stories.tsx
Normal file
25
src/room/LayoutToggle.stories.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
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 { fn } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { LayoutToggle } from "./LayoutToggle";
|
||||
|
||||
const meta = {
|
||||
component: LayoutToggle,
|
||||
} satisfies Meta<typeof LayoutToggle>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
layout: "grid",
|
||||
setLayout: fn(),
|
||||
},
|
||||
};
|
||||
59
src/room/LayoutToggle.tsx
Normal file
59
src/room/LayoutToggle.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2023, 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ChangeEvent, type FC, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
import {
|
||||
SpotlightIcon,
|
||||
GridIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import classNames from "classnames";
|
||||
|
||||
import styles from "./LayoutToggle.module.css";
|
||||
|
||||
export type Layout = "spotlight" | "grid";
|
||||
|
||||
type Props = {
|
||||
layout: Layout;
|
||||
setLayout: (layout: Layout) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => setLayout(e.target.value as Layout),
|
||||
[setLayout],
|
||||
);
|
||||
|
||||
return (
|
||||
<form className={classNames(styles.toggle, className)}>
|
||||
<Tooltip label={t("layout_spotlight_label")}>
|
||||
<input
|
||||
type="radio"
|
||||
name="layout"
|
||||
value="spotlight"
|
||||
checked={layout === "spotlight"}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
<SpotlightIcon aria-hidden width={24} height={24} />
|
||||
<Tooltip label={t("layout_grid_label")}>
|
||||
<input
|
||||
type="radio"
|
||||
name="layout"
|
||||
value="grid"
|
||||
checked={layout === "grid"}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
<GridIcon aria-hidden width={24} height={24} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -396,23 +396,21 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<fieldset
|
||||
aria-label="Layout"
|
||||
class="_toggle_13rnk_9 layout"
|
||||
data-size="lg"
|
||||
<form
|
||||
class="toggle layout"
|
||||
>
|
||||
<input
|
||||
aria-labelledby="_r_11_"
|
||||
name="layoutMode"
|
||||
name="layout"
|
||||
type="radio"
|
||||
value="spotlight"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
@@ -422,23 +420,23 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
<input
|
||||
aria-labelledby="_r_16_"
|
||||
checked=""
|
||||
name="layoutMode"
|
||||
name="layout"
|
||||
type="radio"
|
||||
value="grid"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4 11a.97.97 0 0 1-.712-.287A.97.97 0 0 1 3 10V4q0-.424.288-.712A.97.97 0 0 1 4 3h6q.424 0 .713.288Q11 3.575 11 4v6q0 .424-.287.713A.97.97 0 0 1 10 11zm5-2V5H5v4zm5 12a.97.97 0 0 1-.713-.288A.97.97 0 0 1 13 20v-6q0-.424.287-.713A.97.97 0 0 1 14 13h6q.424 0 .712.287.288.288.288.713v6q0 .424-.288.712A.97.97 0 0 1 20 21zm5-2v-4h-4v4zM4 21a.97.97 0 0 1-.712-.288A.97.97 0 0 1 3 20v-6q0-.424.288-.713A.97.97 0 0 1 4 13h6q.424 0 .713.287.287.288.287.713v6q0 .424-.287.712A.97.97 0 0 1 10 21zm5-2v-4H5v4zm5-8a.97.97 0 0 1-.713-.287A.97.97 0 0 1 13 10V4q0-.424.287-.712A.97.97 0 0 1 14 3h6q.424 0 .712.288Q21 3.575 21 4v6q0 .424-.288.713A.97.97 0 0 1 20 11zm5-2V5h-4v4z"
|
||||
/>
|
||||
</svg>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -778,19 +778,6 @@ export function enterRTCSession(
|
||||
};
|
||||
}
|
||||
|
||||
// Calculates `maximumNetworkErrorRetryCount`. The connection is failed if EITHER:
|
||||
// - The /sync loop is unresponsive for > `gracePeriod` ms, or
|
||||
// - A delayed leave event is emitted (after `leaveDelay` ms period).
|
||||
// Note: Use leaveDelay >> gracePeriod for delegated leave events.
|
||||
const gracePeriod = Config.get().sync_disconnect_grace_period_ms;
|
||||
const leaveDelay = matrixRtcSessionConfig?.delayed_leave_event_delay_ms;
|
||||
const retryInterval = matrixRtcSessionConfig?.network_error_retry_ms;
|
||||
|
||||
// Math.min is used to account for the respective worst case: /sync not available or leave event emitted.
|
||||
const maxWaitTime = Math.min(gracePeriod, leaveDelay);
|
||||
const maximumNetworkErrorRetryCount =
|
||||
Math.ceil(maxWaitTime / retryInterval) + 1;
|
||||
|
||||
// Multi-sfu does not need a preferred foci list. just the focus that is actually used.
|
||||
// TODO where/how do we track errors originating from the ongoing rtcSession?
|
||||
|
||||
@@ -816,7 +803,6 @@ export function enterRTCSession(
|
||||
membershipEventExpiryMs:
|
||||
matrixRtcSessionConfig?.membership_event_expiry_ms,
|
||||
unstableSendStickyEvents: matrixRTCMode === MatrixRTCMode.Matrix_2_0,
|
||||
maximumNetworkErrorRetryCount: maximumNetworkErrorRetryCount,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user