Merge branch 'livekit' into valere/async_error_show_boundary

This commit is contained in:
Valere
2025-03-13 11:20:32 +01:00
58 changed files with 1191 additions and 427 deletions

17
.github/workflows/blocked.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Prevent blocked
on:
pull_request:
types: [opened, labeled, unlabeled]
jobs:
prevent-blocked:
name: Prevent blocked
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v7
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
core.setFailed("PR has been labeled with X-Blocked; it cannot be merged.");

View File

@@ -30,7 +30,7 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ inputs.artifact_run_id }}
name: build-output
name: build-output-full
path: dist
- name: Log in to container registry

View File

@@ -5,6 +5,10 @@ on:
vite_app_version:
required: true
type: string
package:
type: string # This would ideally be a `choice` type, but that isn't supported yet
description: The package type to be built. Must be one of 'full' or 'embedded'
required: true
secrets:
SENTRY_ORG:
required: true
@@ -31,8 +35,20 @@ jobs:
node-version-file: ".node-version"
- name: Install dependencies
run: "yarn install --frozen-lockfile"
- name: Build
run: "yarn run build"
- name: Build full version
if: ${{ inputs.package == 'full' }}
run: "yarn run build:full"
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
VITE_APP_VERSION: ${{ inputs.vite_app_version }}
NODE_OPTIONS: "--max-old-space-size=4096"
- name: Build embedded
if: ${{ inputs.package == 'embedded' }}
run: "yarn run build:embedded"
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
@@ -44,7 +60,7 @@ jobs:
- name: Upload Artifact
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
with:
name: build-output
name: build-output-${{ inputs.package }}
path: dist
# We'll only use this in a triggered job, then we're done with it
retention-days: 1

View File

@@ -16,9 +16,11 @@ on:
- ".github/CODEOWNERS"
- "docs/**"
jobs:
build_element_call:
build_full_element_call:
# Use the full package vite build
uses: ./.github/workflows/build-element-call.yaml
with:
package: full
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
secrets:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
@@ -29,7 +31,7 @@ jobs:
deploy_develop:
# Deploy livekit branch to call.element.dev after build completes
if: github.ref == 'refs/heads/livekit'
needs: build_element_call
needs: build_full_element_call
runs-on: ubuntu-latest
steps:
- name: Deploy to call.element.dev
@@ -50,7 +52,7 @@ jobs:
docker_for_develop:
# Build docker and publish docker for livekit branch after build completes
if: github.ref == 'refs/heads/livekit'
needs: build_element_call
needs: build_full_element_call
permissions:
contents: write
packages: write
@@ -61,3 +63,15 @@ jobs:
type=sha,format=short,event=branch
type=raw,value=latest-ci
type=raw,value=latest-ci_{{date 'X' }}
build_embedded_element_call:
# Use the embedded package vite build
uses: ./.github/workflows/build-element-call.yaml
with:
package: embedded
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
secrets:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -50,7 +50,7 @@ jobs:
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
run-id: ${{ inputs.artifact_run_id }}
name: build-output
name: build-output-full
path: webapp
- name: Add redirects file

View File

@@ -13,6 +13,7 @@ jobs:
if: ${{ github.event_name == 'release' }}
uses: ./.github/workflows/build-element-call.yaml
with:
package: full
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
secrets:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
@@ -38,7 +39,7 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id || github.run_id }}
name: build-output
name: build-output-full
path: dist
- name: Create Tarball
env:

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ node_modules
dist
dist-ssr
*.local
*.bkp
.idea/
public/config.json
backend/synapse_tmp/*

238
README.md
View File

@@ -2,122 +2,101 @@
[![Chat](https://img.shields.io/matrix/webrtc:matrix.org)](https://matrix.to/#/#webrtc:matrix.org)
[![Localazy](https://img.shields.io/endpoint?url=https%3A%2F%2Fconnect.localazy.com%2Fstatus%2Felement-call%2Fdata%3Fcontent%3Dall%26title%3Dlocalazy%26logo%3Dtrue)](https://localazy.com/p/element-call)
[![License](https://img.shields.io/github/license/element-hq/element-call)](LICENSE-AGPL-3.0)
Group calls with WebRTC that leverage [Matrix](https://matrix.org) and an
open-source WebRTC toolkit from [LiveKit](https://livekit.io/).
[🎬 Live Demo 🎬](https://call.element.io)
For prior version of the Element Call that relied solely on full-mesh logic,
check [`full-mesh`](https://github.com/element-hq/element-call/tree/full-mesh)
branch.
The world's first 🌐 decentralized and 🤝 federated video conferencing solution
powered by **the Matrix protocol**.
## 📌 Overview
**Element Call** is a native Matrix video conferencing application developed by
[Element](https://element.io/), designed for **secure**, **scalable**,
**privacy-respecting**, and **decentralized** video and voice calls over the
Matrix protocol. Built on **MatrixRTC**
([MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)), it
utilizes
**[MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)**
with **[LiveKit](https://livekit.io/)** as its backend.
![A demo of Element Call with six people](demo.jpg)
To try it out, visit our hosted version at
[call.element.io](https://call.element.io). You can also find the latest
development version continuously deployed to
You can find the latest development version continuously deployed to
[call.element.dev](https://call.element.dev/).
## Host it yourself
> [!NOTE]
> For prior version of the Element Call that relied solely on full-mesh logic,
> check [`full-mesh`](https://github.com/element-hq/element-call/tree/full-mesh)
> branch.
Until prebuilt tarballs are available, you'll need to build Element Call from
source. First, clone and install the package:
## ✨ Key Features
```sh
git clone https://github.com/element-hq/element-call.git
cd element-call
yarn
yarn build
```
**Decentralized & Federated** No central authority; works across Matrix
homeservers.
**End-to-End Encrypted** Secure and private calls.
**Standalone & Widget Mode** Use as an independent app or embed in Matrix
clients.
**WebRTC-based** No additional software required.
**Scalable with LiveKit** Supports large meetings via SFU
([MSC4195: MatrixRTC using LiveKit backend](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)).
**Raise Hand** Participants can signal when they want to speak, helping to
organize the flow of the meeting.
**Emoji Reactions** Users can react with emojis 👍️ 🎉 👏 🤘, adding
engagement and interactivity to the conversation.
If all went well, you can now find the build output under `dist` as a series of
static files. These can be hosted using any web server that can be configured
with custom routes (see below).
## 🚀 Deployment Options
You also need to add a configuration file which goes in `public/config.json` -
you can use the sample as a starting point:
Element Call can be packaged in two ways:
```sh
cp config/config.sample.json public/config.json
# edit public/config.json
```
**Full Package** Supports both **Standalone** and **Widget** mode. Hosted as
a static web page and accessed via a URL when used as a widget.
The sample needs editing to contain the homeserver and LiveKit backend that you
are using.
**Embedded Package** 🚧 **Coming [Soon](https://github.com/element-hq/element-call/issues/2994):** Designed for **Widget mode** only. Bundled with a
messenger app for seamless integration. This is the recommended method for
embedding Element Call into a messenger app.
Because Element Call uses client-side routing, your server must be able to route
any requests to non-existing paths back to `/index.html`. For example, in Nginx
you can achieve this with the `try_files` directive:
### Standalone mode
```jsonc
server {
...
location / {
...
try_files $uri /$uri /index.html;
}
}
```
![Element Call in Standalone Mode](./docs/element_call_standalone.drawio.png)
Element Call requires a homeserver with registration enabled without any 3pid or
token requirements, if you want it to be used by unregistered users.
Furthermore, it is not recommended to use it with an existing homeserver where
user accounts have joined normal rooms, as it may not be able to handle those
yet and it may behave unreliably.
In Standalone mode Element Call operates as an independent, full-featured video
conferencing web application, allowing users to join or host calls without
requiring a separate Matrix client.
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
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
rooms that Element Call cannot handle.
### Widget mode embedded in Messenger Apps
## Configuration
![Element Call in Widget Mode](./docs/element_call_widget.drawio.png)
There are currently two different config files. `.env` holds variables that are
used at build time, while `public/config.json` holds variables that are used at
runtime. Documentation and default values for `public/config.json` can be found
in [ConfigOptions.ts](src/config/ConfigOptions.ts).
Element Call can be embedded as a widget inside apps like
[**Element Web**](https://github.com/element-hq/element-web) or **Element X
([iOS](https://github.com/element-hq/element-x-ios),
[Android](https://github.com/element-hq/element-x-android))**, bringing
**MatrixRTC** capabilities to messenger apps for seamless decentralized video
and voice calls within Matrix rooms.
If you're using [Synapse](https://github.com/element-hq/synapse/), you'll need
to additionally add the following to `homeserver.yaml` or Element Call won't
work:
> [!IMPORTANT]
> Embedded packaging is recommended for Element Call in widget mode!
```yaml
experimental_features:
# MSC3266: Room summary API. Used for knocking over federation
msc3266_enabled: true
# MSC4222 needed for syncv2 state_after. This allow clients to
# correctly track the state of the room.
msc4222_enabled: true
## 🛠️ Self-Hosting
# The maximum allowed duration by which sent events can be delayed, as
# per MSC4140.
max_event_delay_duration: 24h
For operating and deploying Element Call on your own server, refer to the
[**Self-Hosting Guide**](./docs/self-hosting.md).
rc_message:
# 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
per_second: 0.5
burst_count: 30
```
## 🧭 MatrixRTC Backend Discovery and Selection
MSC3266 allows to request a room summary of rooms you are not joined. The
summary contains the room join rules. We need that to decide if the user gets
prompted with the option to knock ("Request to join call"), a cannot join error or the
join view.
For proper Element Call operation each site deployment needs a MatrixRTC backend
setup as outlined in the [Self-Hosting](#self-hosting). A typical federated site
deployment for three different sites A, B and C is depicted below.
MSC4222 allow clients to opt-in to a change of the sync v2 API that allows them
to correctly track the state of the room. This is required by Element Call to
track room state reliably.
![Element Call federated setup](./docs/Federated_Setup.drawio.png)
Element Call requires a Livekit SFU alongside a [Livekit JWT
service](https://github.com/element-hq/lk-jwt-service) to work. The url to the
Livekit JWT service can either be configured in the config of Element Call
(fallback/legacy configuration) or be configured by your homeserver via the
`.well-known/matrix/client`. This is the recommended method.
### Backend Discovery
The configuration is a list of Foci configs:
MatrixRTC backend (according to
[MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143))
is announced by the homeserver's `.well-known/matrix/client` file and discovered
via the `org.matrix.msc4143.rtc_foci` key, e.g.:
```json
"org.matrix.msc4143.rtc_foci": [
@@ -125,25 +104,41 @@ The configuration is a list of Foci configs:
"type": "livekit",
"livekit_service_url": "https://someurl.com"
},
{
"type": "livekit",
"livekit_service_url": "https://livekit2.com"
},
{
"type": "another_foci",
"props_for_another_foci": "val"
},
]
```
## Translation
where the format for MatrixRTC using LiveKit backend is defined in
[MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md).
In the example above Matrix clients do discover a focus of type `livekit` which
points them to a Matrix LiveKit JWT Auth Service via `livekit_service_url`.
### Backend Selection
- Each call participant proposes their discovered MatrixRTC backend from
`org.matrix.msc4143.rtc_foci` in their `org.matrix.msc3401.call.member` state event.
- For **LiveKit** MatrixRTC backend
([MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)),
the **first participant who joined the call** defines via the `foci_preferred`
key in their `org.matrix.msc3401.call.member` which actual MatrixRTC backend
will be used for this call.
- During the actual call join flow, the **LiveKit JWT Auth Service** provides
the client with the **LiveKit SFU WebSocket URL** and an **access JWT token**
in order to exchange media via WebRTC.
The example below illustrates how backend selection works across **Matrix
federation**, using the setup from sites A, B, and C. It demonstrates backend
selection for **Matrix rooms 123 and 456**, which include users from different
homeservers.
![Element Call SFU selection over Matrix federation](./docs/SFU_selection.drawio.png)
## 🌍 Translation
If you'd like to help translate Element Call, head over to
[Localazy](https://localazy.com/p/element-call). You're also encouraged to join
the [Element Translators](https://matrix.to/#/#translators:element.io) space to
discuss and coordinate translation efforts.
## Development
## 🛠️ Development
### Frontend
@@ -155,13 +150,15 @@ cd element-call
yarn
```
To use it, create a local config by, e.g., `cp ./config/config.devenv.json ./public/config.json`
and adapt it if necessary. The `config.devenv.json` config should work with the backend
development environment as outlined in the next section out of box.
To use it, create a local config by, e.g.,
`cp ./config/config.devenv.json ./public/config.json` and adapt it if necessary.
The `config.devenv.json` config should work with the backend development
environment as outlined in the next section out of box.
(Be aware, that this `config.devenv.json` is exposing a deprecated fallback
LiveKit config key. If the homeserver advertises SFU backend via
`.well-known/matrix/client` this has precedence.)
> [!NOTE]
> Be aware, that this `config.devenv.json` is exposing a deprecated fallback
> LiveKit config key. If the homeserver advertises SFU backend via
> `.well-known/matrix/client` this has precedence.
You're now ready to launch the development server:
@@ -200,7 +197,8 @@ yarn backend
To add a new translation key you can do these steps:
1. Add the new key entry to the code where the new key is used: `t("some_new_key")`
1. Add the new key entry to the code where the new key is used:
`t("some_new_key")`
1. Run `yarn i18n` to extract the new key and update the translation files. This
will add a skeleton entry to the `locales/en/app.json` file:
@@ -212,30 +210,38 @@ To add a new translation key you can do these steps:
}
```
1. Update the skeleton entry in the `locales/en/app.json` file with
the English translation:
1. Update the skeleton entry in the `locales/en/app.json` file with the English
translation:
```jsonc
```jsonc
{
...
"some_new_key": "Some new key",
...
}
```
```
## Documentation
## 📖 Documentation
Usage and other technical details about the project can be found here:
[**Docs**](./docs/README.md)
## Copyright & License
## 📝 Copyright & License
Copyright 2021-2025 New Vector Ltd
This software is dual-licensed by New Vector Ltd (Element). It can be used either:
This software is dual-licensed by New Vector Ltd (Element). It can be used
either:
(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
(1) for free under the terms of the GNU Affero General Public License (as
published by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version); OR
(2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to).
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.
(2) under the terms of a paid-for Element Commercial License agreement between
you and Element (the terms of which may vary depending on what you and Element
have agreed to). Unless required by applicable law or agreed to in writing,
software distributed under the Licenses is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
Licenses for the specific language governing permissions and limitations under
the Licenses.

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -5,3 +5,4 @@ This folder contains documentation for Element Call setup and usage.
- [Embedded vs standalone mode](./embedded-standalone.md)
- [Url format and parameters](./url-params.md)
- [Global JS controls](./controls.md)
- [Self-Hosting](./self-hosting.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

View File

@@ -2,7 +2,7 @@
Element call is developed using the js-sdk with matroska mode. This means the app can run either as a standalone app directly connected to a homeserver providing login interfaces or it can be used as a widget.
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.
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 element call will try to connect to the client via the widget postMessage api using the parameters provided in [Url Format and parameters

161
docs/self-hosting.md Normal file
View File

@@ -0,0 +1,161 @@
# Self-Hosting Element Call
## Prerequisites
> [!IMPORTANT]
> This section covers the requirements for deploying a **Matrix site**
> compatible with MatrixRTC, the foundation of Element Call. These requirements
> apply to both Standalone as well as Widget mode operation of Element Call.
### A Matrix Homeserver
The following [MSCs](https://github.com/matrix-org/matrix-spec-proposals) are
required for Element Call to work properly:
- **[MSC3266](https://github.com/deepbluev7/matrix-doc/blob/room-summaries/proposals/3266-room-summary.md):
Room Summary API**: In Standalone mode Element Call is able to join rooms
over federation using knocking. In this context MSC3266 is required as it
allows to request a room summary of rooms you are not joined. The summary
contains the room join rules. We need that information to decide if the user
gets prompted with the option to knock ("Request to join call"), a "cannot
join error" or "the join view".
- **[MSC4140](https://github.com/matrix-org/matrix-spec-proposals/blob/toger5/expiring-events-keep-alive/proposals/4140-delayed-events-futures.md)
Delayed Events**: Delayed events are required for proper call participation
signalling. If disabled it is very likely that you end up with stuck calls in
Matrix rooms.
- **[MSC4222](https://github.com/matrix-org/matrix-spec-proposals/blob/erikj/sync_v2_state_after/proposals/4222-sync-v2-state-after.md)
Adding `state_after` to sync v2**: Allow clients to opt-in to a change of the
sync v2 API that allows them to correctly track the state of the room. This is
required by Element Call to track room state reliably.
If you're using [Synapse](https://github.com/element-hq/synapse/) as your homeserver, you'll need
to additionally add the following config items to `homeserver.yaml` to comply with Element Call:
```yaml
experimental_features:
# MSC3266: Room summary API. Used for knocking over federation
msc3266_enabled: true
# MSC4222 needed for syncv2 state_after. This allow clients to
# correctly track the state of the room.
msc4222_enabled: true
# The maximum allowed duration by which sent events can be delayed, as
# per MSC4140.
max_event_delay_duration: 24h
rc_message:
# 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
per_second: 0.5
burst_count: 30
```
### MatrixRTC Backend
In order to **guarantee smooth operation** of Element Call MatrixRTC backend is
required for each site deployment.
![MSC4195 compatible setup](MSC4195_setup.drawio.png)
As depicted above, Element Call requires a
[Livekit SFU](https://github.com/livekit/livekit) alongside a
[Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service)
to implement
[MSC4195: MatrixRTC using LiveKit backend](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md).
> [!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 **homeserver's
> `.well-known/matrix/client`**. The configuration is a list of Foci configs:
```json
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://someurl.com"
},
{
"type": "livekit",
"livekit_service_url": "https://livekit2.com"
},
{
"type": "another_foci",
"props_for_another_foci": "val"
},
]
```
## Building Element Call
> [!NOTE]
> This step is only required if you want to deploy Element Call in Standalone
> mode.
Until prebuilt tarballs are available, you'll need to build Element Call from
source. First, clone and install the package:
```sh
git clone https://github.com/element-hq/element-call.git
cd element-call
yarn
yarn build
```
If all went well, you can now find the build output under `dist` as a series of
static files. These can be hosted using any web server that can be configured
with custom routes (see below).
You also need to add a configuration file which goes in `public/config.json` -
you can use the sample as a starting point:
```sh
cp config/config.sample.json public/config.json
# edit public/config.json
```
The sample needs editing to contain the homeserver that you are using.
Because Element Call uses client-side routing, your server must be able to route
any requests to non-existing paths back to `/index.html`. For example, in Nginx
you can achieve this with the `try_files` directive:
```jsonc
server {
...
location / {
...
try_files $uri /$uri /index.html;
}
}
```
## Configuration
There are currently two different config files. `.env` holds variables that are
used at build time, while `public/config.json` holds variables that are used at
runtime. Documentation and default values for `public/config.json` can be found
in [ConfigOptions.ts](src/config/ConfigOptions.ts).
> [!CAUTION]
> Please note configuring MatrixRTC backend via `config.json` of
> Element Call is only available for developing and debug purposes. Relying on
> it might break Element Call going forward!
## A Note on Standalone Mode of Element Call
Element Call in Standalone mode requires a homeserver with registration enabled
without any 3pid or token requirements, if you want it to be used by
unregistered users. Furthermore, it is not recommended to use it with an
existing homeserver where user accounts have joined normal rooms, as it may not
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
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
rooms that Element Call cannot handle.

View File

@@ -2,34 +2,40 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<% if (packageType === "full") { %>
<link rel="icon" type="image/svg+xml" href="favicon.png" />
<link rel="preload" href="/config.json" as="fetch" />
<% } %>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
/>
<title><%- title %></title>
<title><%- brand %></title>
<script>
window.global = window;
</script>
<% if (packageType === "full") { %>
<!-- Open graph meta tags -->
<meta property="og:title" content="<%- title %>" />
<meta property="og:title" content="<%- brand %>" />
<meta
property="og:description"
content="You're invited to join a call on Element Call"
content="You're invited to join a call on <%- brand %>"
/>
<meta property="og:type" content="website" />
<meta property="og:image" content="favicon.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="<%- title %>" />
<meta name="twitter:title" content="<%- brand %>" />
<meta
name="twitter:description"
content="You're invited to join a call on Element Call"
content="You're invited to join a call on <%- brand %>"
/>
<meta name="twitter:image" content="favicon.png" />
<% } %>
</head>
<!-- The default class is: .no-theme {display: none}. It will be overwritten once the app is loaded. -->

View File

@@ -1,6 +1,9 @@
import { KnipConfig } from "knip";
export default {
vite: {
config: ["vite.config.js", "vite-embedded.config.js"],
},
entry: ["src/main.tsx", "i18next-parser.config.ts"],
ignoreBinaries: [
// This is deprecated, so Knip doesn't actually recognize it as a globally

View File

@@ -70,7 +70,8 @@
"livekit_sfu": "LiveKit SFU: {{url}}",
"matrix_id": "Matrix-ID: {{id}}",
"show_connection_stats": "Verbindungsstatistiken anzeigen",
"show_non_member_tiles": "Kacheln für Nicht-Mitgliedermedien anzeigen"
"show_non_member_tiles": "Kacheln für Nicht-Mitgliedermedien anzeigen",
"use_new_membership_manager": "Neuen MembershipManager verwenden"
},
"disconnected_banner": "Die Verbindung zum Server wurde getrennt.",
"error": {
@@ -87,7 +88,7 @@
"insufficient_capacity_description": "Der Server hat seine maximale Kapazität erreicht, daher ist ein Beitritt zum Anruf derzeit nicht möglich. Bitte später erneut versuchen oder den Serveradministrator kontaktieren, falls das Problem weiterhin besteht.",
"matrix_rtc_focus_missing": "Der Server ist nicht für die Verwendung mit {{brand}} konfiguriert. Bitte den Serveradministrator kontaktieren (Domain: {{domain}}, Fehlercode: {{ errorCode }}).",
"open_elsewhere": "In einem anderen Tab geöffnet",
"open_elsewhere_description": "{{brand}}wurde in einem anderen Tab geöffnet. Wenn das nicht richtig klingt, versuchen Sie, die Seite neu zu laden.",
"open_elsewhere_description": "{{brand}} wurde in einem anderen Tab geöffnet. Wenn das nicht richtig klingt, versuchen Sie, die Seite neu zu laden.",
"unexpected_ec_error": "Ein unerwarteter Fehler ist aufgetreten (<0>Fehlercode: </0> <1>{{ errorCode }}</1>). Bitte den Serveradministrator kontaktieren."
},
"group_call_loader": {

View File

@@ -70,7 +70,8 @@
"livekit_sfu": "LiveKit SFU: {{url}}",
"matrix_id": "Matrix ID: {{id}}",
"show_connection_stats": "Show connection statistics",
"show_non_member_tiles": "Show tiles for non-member media"
"show_non_member_tiles": "Show tiles for non-member media",
"use_new_membership_manager": "Use the new implementation of the call MembershipManager"
},
"disconnected_banner": "Connectivity to the server has been lost.",
"error": {

View File

@@ -3,8 +3,12 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "NODE_OPTIONS=--max-old-space-size=16384 vite build",
"dev": "yarn dev:full",
"dev:full": "vite",
"dev:embedded": "vite --config vite-embedded.config.js",
"build": "yarn build:full",
"build:full": "NODE_OPTIONS=--max-old-space-size=16384 vite build",
"build:embedded": "yarn build:full --config vite-embedded.config.js",
"serve": "vite preview",
"prettier:check": "prettier -c .",
"prettier:format": "prettier -w .",
@@ -91,7 +95,7 @@
"livekit-client": "^2.5.7",
"lodash-es": "^4.17.21",
"loglevel": "^1.9.1",
"matrix-js-sdk": "^36.1.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#9f9be701e7a8e85b5f749d0104138af36b0b82bd",
"matrix-widget-api": "1.11.0",
"normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3",
@@ -114,7 +118,8 @@
"unique-names-generator": "^4.6.0",
"vaul": "^1.0.0",
"vite": "^6.0.0",
"vite-plugin-html-template": "^1.1.0",
"vite-plugin-generate-file": "^0.2.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-svgr": "^4.0.0",
"vitest": "^3.0.0",
"vitest-axe": "^1.0.0-pre.3"

View File

@@ -6,6 +6,8 @@ Please see LICENSE in the repository root for full details.
*/
import "matrix-js-sdk/src/@types/global";
import { type setLogLevel as setLKLogLevel } from "livekit-client";
import type { DurationFormat as PolyfillDurationFormat } from "@formatjs/intl-durationformat";
import { type Controls } from "../controls";
@@ -18,6 +20,7 @@ declare global {
interface Window {
controls: Controls;
setLKLogLevel: typeof setLKLogLevel;
}
interface HTMLElement {

View File

@@ -72,7 +72,11 @@ export const App: FC = () => {
<Suspense fallback={null}>
<ClientProvider>
<MediaDevicesProvider>
<Sentry.ErrorBoundary fallback={ErrorPage}>
<Sentry.ErrorBoundary
fallback={(error) => (
<ErrorPage error={error} widget={widget} />
)}
>
<DisconnectedBanner />
<Routes>
<SentryRoute path="/" element={<HomePage />} />

View File

@@ -351,7 +351,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
}, [initClientState, onSync]);
if (alreadyOpenedErr) {
return <ErrorPage error={alreadyOpenedErr} />;
return <ErrorPage widget={widget} error={alreadyOpenedErr} />;
}
return (

View File

@@ -12,13 +12,16 @@ import {
type FC,
type ReactNode,
type SVGAttributes,
type ReactElement,
} from "react";
import { useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger";
import { RageshakeButton } from "./settings/RageshakeButton";
import styles from "./ErrorView.module.css";
import { useUrlParams } from "./UrlParams";
import { LinkButton } from "./button";
import { ElementWidgetActions, type WidgetHelpers } from "./widget.ts";
interface Props {
Icon: ComponentType<SVGAttributes<SVGElement>>;
@@ -35,6 +38,7 @@ interface Props {
*/
fatal?: boolean;
children: ReactNode;
widget: WidgetHelpers | null;
}
export const ErrorView: FC<Props> = ({
@@ -43,6 +47,7 @@ export const ErrorView: FC<Props> = ({
rageshake,
fatal,
children,
widget,
}) => {
const { t } = useTranslation();
const { confineToRoom } = useUrlParams();
@@ -51,6 +56,46 @@ export const ErrorView: FC<Props> = ({
window.location.href = "/";
}, []);
const CloseWidgetButton: FC<{ widget: WidgetHelpers }> = ({
widget,
}): ReactElement => {
// in widget mode we don't want to show the return home button but a close button
const closeWidget = (): void => {
widget.api.transport
.send(ElementWidgetActions.Close, {})
.catch((e) => {
// What to do here?
logger.error("Failed to send close action", e);
})
.finally(() => {
widget.api.transport.stop();
});
};
return (
<Button kind="primary" onClick={closeWidget}>
{t("action.close")}
</Button>
);
};
// Whether the error is considered fatal or pathname is `/` then reload the all app.
// If not then navigate to home page.
const ReturnToHomeButton = (): ReactElement => {
if (fatal || location.pathname === "/") {
return (
<Button kind="tertiary" className={styles.homeLink} onClick={onReload}>
{t("return_home_button")}
</Button>
);
} else {
return (
<LinkButton kind="tertiary" className={styles.homeLink} to="/">
{t("return_home_button")}
</LinkButton>
);
}
};
return (
<div className={styles.error}>
<BigIcon className={styles.icon}>
@@ -63,20 +108,11 @@ export const ErrorView: FC<Props> = ({
{rageshake && (
<RageshakeButton description={`***Error View***: ${title}`} />
)}
{!confineToRoom &&
(fatal || location.pathname === "/" ? (
<Button
kind="tertiary"
className={styles.homeLink}
onClick={onReload}
>
{t("return_home_button")}
</Button>
) : (
<LinkButton kind="tertiary" className={styles.homeLink} to="/">
{t("return_home_button")}
</LinkButton>
))}
{widget ? (
<CloseWidgetButton widget={widget} />
) : (
!confineToRoom && <ReturnToHomeButton />
)}
</div>
);
};

View File

@@ -17,6 +17,7 @@ import styles from "./FullScreenView.module.css";
import { useUrlParams } from "./UrlParams";
import { RichError } from "./RichError";
import { ErrorView } from "./ErrorView";
import { type WidgetHelpers } from "./widget.ts";
interface FullScreenViewProps {
className?: string;
@@ -47,11 +48,12 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
interface ErrorPageProps {
error: Error | unknown;
widget: WidgetHelpers | null;
}
// Due to this component being used as the crash fallback for Sentry, which has
// weird type requirements, we can't just give this a type of FC<ErrorPageProps>
export const ErrorPage = ({ error }: ErrorPageProps): ReactElement => {
export const ErrorPage = ({ error, widget }: ErrorPageProps): ReactElement => {
const { t } = useTranslation();
useEffect(() => {
logger.error(error);
@@ -63,7 +65,13 @@ export const ErrorPage = ({ error }: ErrorPageProps): ReactElement => {
{error instanceof RichError ? (
error.richMessage
) : (
<ErrorView Icon={ErrorIcon} title={t("error.generic")} rageshake fatal>
<ErrorView
widget={widget}
Icon={ErrorIcon}
title={t("error.generic")}
rageshake
fatal
>
<p>{t("error.generic_description")}</p>
</ErrorView>
)}

View File

@@ -10,6 +10,7 @@ import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import type { FC, ReactNode } from "react";
import { ErrorView } from "./ErrorView";
import { widget } from "./widget.ts";
/**
* An error consisting of a terse message to be logged to the console and a
@@ -31,7 +32,11 @@ const OpenElsewhere: FC = () => {
const { t } = useTranslation();
return (
<ErrorView Icon={PopOutIcon} title={t("error.open_elsewhere")}>
<ErrorView
widget={widget}
Icon={PopOutIcon}
title={t("error.open_elsewhere")}
>
<p>
{t("error.open_elsewhere_description", {
brand: import.meta.env.VITE_PRODUCT_NAME || "Element Call",

View File

@@ -110,8 +110,8 @@ describe("UrlParams", () => {
});
describe("returnToLobby", () => {
it("is true in SPA mode", () => {
expect(getUrlParams("?returnToLobby=false").returnToLobby).toBe(true);
it("is false in SPA mode", () => {
expect(getUrlParams("?returnToLobby=true").returnToLobby).toBe(false);
});
it("defaults to false in widget mode", () => {

View File

@@ -264,7 +264,9 @@ export const getUrlParams = (
"skipLobby",
isWidget && intent === UserIntent.StartNewCall,
),
returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : true,
// In SPA mode the user should always exit to the home screen when hanging
// up, rather than being sent back to the lobby
returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : false,
theme: parser.getParam("theme"),
viaServers: !isWidget ? parser.getParam("viaServers") : null,
homeserver: !isWidget ? parser.getParam("homeserver") : null,

View File

@@ -13,6 +13,7 @@ import {
type ConfigOptions,
type ResolvedConfigOptions,
} from "./ConfigOptions";
import { isFailure } from "../utils/fetch";
export class Config {
private static internalInstance: Config | undefined;
@@ -28,7 +29,20 @@ export class Config {
const internalInstance = new Config();
Config.internalInstance = internalInstance;
Config.internalInstance.initPromise = downloadConfig("/config.json").then(
let fetchTarget: string;
if (
window.location.pathname.endsWith("/room/") ||
window.location.pathname.endsWith("/room")
) {
// it looks like we are running in standalone mode so use the config at the root
fetchTarget = new URL("/config.json", window.location.href).href;
} else {
// otherwise we are probably running as a widget so use the config in the same directory
fetchTarget = "config.json";
}
Config.internalInstance.initPromise = downloadConfig(fetchTarget).then(
(config) => {
internalInstance.config = merge({}, DEFAULT_CONFIG, config);
},
@@ -70,18 +84,15 @@ export class Config {
private initPromise?: Promise<void>;
}
async function downloadConfig(
configJsonFilename: string,
): Promise<ConfigOptions> {
const url = new URL(configJsonFilename, window.location.href);
const res = await fetch(url);
async function downloadConfig(fetchTarget: string): Promise<ConfigOptions> {
const response = await fetch(fetchTarget);
if (!res.ok || res.status === 404 || res.status === 0) {
if (isFailure(response)) {
// Lack of a config isn't an error, we should just use the defaults.
// Also treat a blank config as no config, assuming the status code is 0, because we don't get 404s from file:
// URIs so this is the only way we can not fail if the file doesn't exist when loading from a file:// URI.
return DEFAULT_CONFIG;
}
return res.json();
return response.json();
}

View File

@@ -76,7 +76,7 @@ export interface ConfigOptions {
/**
* A link to the end-user license agreement (EULA)
*/
eula: string;
eula?: string;
media_devices?: {
/**
@@ -131,6 +131,7 @@ export interface ResolvedConfigOptions extends ConfigOptions {
server_name: string;
};
};
eula: string;
media_devices: {
enable_audio: boolean;
enable_video: boolean;

View File

@@ -13,6 +13,7 @@ import { ErrorPage, LoadingPage } from "../FullScreenView";
import { UnauthenticatedView } from "./UnauthenticatedView";
import { RegisteredView } from "./RegisteredView";
import { usePageTitle } from "../usePageTitle";
import { widget } from "../widget.ts";
export const HomePage: FC = () => {
const { t } = useTranslation();
@@ -23,7 +24,7 @@ export const HomePage: FC = () => {
if (!clientState) {
return <LoadingPage />;
} else if (clientState.state === "error") {
return <ErrorPage error={clientState.error} />;
return <ErrorPage widget={widget} error={clientState.error} />;
} else {
return clientState.authenticated ? (
<RegisteredView client={clientState.authenticated.client} />

View File

@@ -28,6 +28,7 @@ import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config";
import { ElementCallOpenTelemetry } from "./otel/otel";
import { platform } from "./Platform";
import { isFailure } from "./utils/fetch";
// This generates a map of locale names to their URL (based on import.meta.url), which looks like this:
// {
@@ -79,7 +80,7 @@ const Backend = {
},
});
if (!response.ok) {
if (isFailure(response)) {
throw Error(`Failed to fetch ${url}`);
}

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
import { type FC, useCallback, useState } from "react";
import { test } from "vitest";
import { test, vi } from "vitest";
import {
ConnectionError,
ConnectionErrorReason,
@@ -60,7 +60,7 @@ test.each<[string, ConnectionError]>([
const user = userEvent.setup();
render(
<MemoryRouter>
<GroupCallErrorBoundary>
<GroupCallErrorBoundary recoveryActionHandler={vi.fn()} widget={null}>
<TestComponent />
</GroupCallErrorBoundary>
</MemoryRouter>,

View File

@@ -24,6 +24,8 @@ import { App } from "./App";
import { init as initRageshake } from "./settings/rageshake";
import { Initializer } from "./initializer";
window.setLKLogLevel = setLKLogLevel;
initRageshake().catch((e) => {
logger.error("Failed to initialize rageshake", e);
});

View File

@@ -12,13 +12,15 @@ import {
type ReactElement,
type ReactNode,
useCallback,
useEffect,
useState,
} from "react";
import { BrowserRouter } from "react-router-dom";
import userEvent from "@testing-library/user-event";
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
import {
type CallErrorRecoveryAction,
GroupCallErrorBoundary,
} from "./GroupCallErrorBoundary.tsx";
import {
ConnectionLostError,
E2EENotSupportedError,
@@ -28,8 +30,7 @@ import {
UnknownCallError,
} from "../utils/errors.ts";
import { mockConfig } from "../utils/test.ts";
import { useGroupCallErrorBoundary } from "./useCallErrorBoundary.ts";
import { GroupCallErrorBoundaryContextProvider } from "./GroupCallErrorBoundaryContextProvider.tsx";
import { ElementWidgetActions, type WidgetHelpers } from "../widget.ts";
test.each([
{
@@ -63,7 +64,11 @@ test.each([
const onErrorMock = vi.fn();
const { asFragment } = render(
<BrowserRouter>
<GroupCallErrorBoundary onError={onErrorMock}>
<GroupCallErrorBoundary
onError={onErrorMock}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
</BrowserRouter>,
@@ -88,14 +93,18 @@ test("should render the error page with link back to home", async () => {
const onErrorMock = vi.fn();
const { asFragment } = render(
<BrowserRouter>
<GroupCallErrorBoundary onError={onErrorMock}>
<GroupCallErrorBoundary
onError={onErrorMock}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
</BrowserRouter>,
);
await screen.findByText("Call is not supported");
expect(screen.getByText(/Domain: example.com/i)).toBeInTheDocument();
expect(screen.getByText(/Domain: example\.com/i)).toBeInTheDocument();
expect(
screen.getByText(/Error Code: MISSING_MATRIX_RTC_FOCUS/i),
).toBeInTheDocument();
@@ -108,39 +117,7 @@ test("should render the error page with link back to home", async () => {
expect(asFragment()).toMatchSnapshot();
});
test("should have a reconnect button for ConnectionLostError", async () => {
const user = userEvent.setup();
const reconnectCallback = vi.fn();
const TestComponent = (): ReactNode => {
throw new ConnectionLostError();
};
const { asFragment } = render(
<BrowserRouter>
<GroupCallErrorBoundary
onError={vi.fn()}
recoveryActionHandler={reconnectCallback}
>
<TestComponent />
</GroupCallErrorBoundary>
</BrowserRouter>,
);
await screen.findByText("Connection lost");
await screen.findByRole("button", { name: "Reconnect" });
await screen.findByRole("button", { name: "Return to home screen" });
expect(asFragment()).toMatchSnapshot();
await user.click(screen.getByRole("button", { name: "Reconnect" }));
expect(reconnectCallback).toHaveBeenCalledOnce();
expect(reconnectCallback).toHaveBeenCalledWith("reconnect");
});
test("Action handling should reset error state", async () => {
test("ConnectionLostError: Action handling should reset error state", async () => {
const user = userEvent.setup();
const TestComponent: FC<{ fail: boolean }> = ({ fail }): ReactNode => {
@@ -150,30 +127,46 @@ test("Action handling should reset error state", async () => {
return <div>HELLO</div>;
};
const reconnectCallbackSpy = vi.fn();
const WrapComponent = (): ReactNode => {
const [failState, setFailState] = useState(true);
const reconnectCallback = useCallback(() => {
setFailState(false);
}, [setFailState]);
const reconnectCallback = useCallback(
(action: CallErrorRecoveryAction) => {
reconnectCallbackSpy(action);
setFailState(false);
},
[setFailState],
);
return (
<BrowserRouter>
<GroupCallErrorBoundary recoveryActionHandler={reconnectCallback}>
<GroupCallErrorBoundary
recoveryActionHandler={reconnectCallback}
widget={null}
>
<TestComponent fail={failState} />
</GroupCallErrorBoundary>
</BrowserRouter>
);
};
render(<WrapComponent />);
const { asFragment } = render(<WrapComponent />);
// Should fail first
await screen.findByText("Connection lost");
await screen.findByRole("button", { name: "Reconnect" });
await screen.findByRole("button", { name: "Return to home screen" });
expect(asFragment()).toMatchSnapshot();
await user.click(screen.getByRole("button", { name: "Reconnect" }));
// reconnect should have reset the error, thus rendering should be ok
await screen.findByText("HELLO");
expect(reconnectCallbackSpy).toHaveBeenCalledOnce();
expect(reconnectCallbackSpy).toHaveBeenCalledWith("reconnect");
});
describe("Rageshake button", () => {
@@ -190,7 +183,11 @@ describe("Rageshake button", () => {
render(
<BrowserRouter>
<GroupCallErrorBoundary onError={vi.fn()}>
<GroupCallErrorBoundary
onError={vi.fn()}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
</BrowserRouter>,
@@ -214,29 +211,43 @@ describe("Rageshake button", () => {
});
});
test("should show async error with useElementCallErrorContext", async () => {
// const error = new MatrixRTCFocusMissingError("example.com");
test("should have a close button in widget mode", async () => {
const error = new MatrixRTCFocusMissingError("example.com");
const TestComponent = (): ReactNode => {
const { showGroupCallErrorBoundary } = useGroupCallErrorBoundary();
useEffect(() => {
setTimeout(() => {
showGroupCallErrorBoundary(new ConnectionLostError());
});
}, [showGroupCallErrorBoundary]);
return <div>Hello</div>;
throw error;
};
const mockWidget = {
api: {
transport: { send: vi.fn().mockResolvedValue(undefined), stop: vi.fn() },
},
} as unknown as WidgetHelpers;
const user = userEvent.setup();
const onErrorMock = vi.fn();
render(
const { asFragment } = render(
<BrowserRouter>
<GroupCallErrorBoundaryContextProvider>
<GroupCallErrorBoundary onError={onErrorMock}>
<TestComponent />
</GroupCallErrorBoundary>
</GroupCallErrorBoundaryContextProvider>
<GroupCallErrorBoundary
widget={mockWidget}
onError={onErrorMock}
recoveryActionHandler={vi.fn()}
>
<TestComponent />
</GroupCallErrorBoundary>
</BrowserRouter>,
);
await screen.findByText("Connection lost");
await screen.findByText("Call is not supported");
await screen.findByRole("button", { name: "Close" });
expect(asFragment()).toMatchSnapshot();
await user.click(screen.getByRole("button", { name: "Close" }));
expect(mockWidget.api.transport.send).toHaveBeenCalledWith(
ElementWidgetActions.Close,
expect.anything(),
);
expect(mockWidget.api.transport.stop).toHaveBeenCalled();
});

View File

@@ -31,6 +31,7 @@ import {
} from "../utils/errors.ts";
import { FullScreenView } from "../FullScreenView.tsx";
import { ErrorView } from "../ErrorView.tsx";
import { type WidgetHelpers } from "../widget.ts";
export type CallErrorRecoveryAction = "reconnect"; // | "retry" ;
@@ -38,17 +39,18 @@ export type RecoveryActionHandler = (action: CallErrorRecoveryAction) => void;
interface ErrorPageProps {
error: ElementCallError;
recoveryActionHandler?: RecoveryActionHandler;
recoveryActionHandler: RecoveryActionHandler;
resetError: () => void;
widget: WidgetHelpers | null;
}
const ErrorPage: FC<ErrorPageProps> = ({
error,
recoveryActionHandler,
widget,
}: ErrorPageProps): ReactElement => {
const { t } = useTranslation();
// let title: string;
let icon: ComponentType<SVGAttributes<SVGElement>>;
switch (error.category) {
case ErrorCategory.CONFIGURATION_ISSUE:
@@ -68,7 +70,7 @@ const ErrorPage: FC<ErrorPageProps> = ({
if (error instanceof ConnectionLostError) {
actions.push({
label: t("call_ended_view.reconnect_button"),
onClick: () => recoveryActionHandler?.("reconnect"),
onClick: () => recoveryActionHandler("reconnect"),
});
}
@@ -78,6 +80,7 @@ const ErrorPage: FC<ErrorPageProps> = ({
Icon={icon}
title={error.localisedTitle}
rageshake={error.code == ErrorCode.UNKNOWN_ERROR}
widget={widget}
>
<p>
{error.localisedMessage ?? (
@@ -101,38 +104,16 @@ const ErrorPage: FC<ErrorPageProps> = ({
interface BoundaryProps {
children: ReactNode | (() => ReactNode);
recoveryActionHandler?: RecoveryActionHandler;
recoveryActionHandler: RecoveryActionHandler;
onError?: (error: unknown) => void;
widget: WidgetHelpers | null;
}
/**
* An ErrorBoundary component that handles ElementCalls errors that can occur during a group call.
* It is based on the sentry ErrorBoundary component, that will log the error to sentry.
*
* The error fallback will show an error page with:
* - a description of the error
* - a button to go back the home screen
* - optional call-to-action buttons (ex: reconnect for connection lost)
* - A rageshake button for unknown errors
*
* For async errors the `useCallErrorBoundary` hook should be used to show the error page
* ```
* const { showGroupCallErrorBoundary } = useCallErrorBoundary();
* ... some async code
* catch(error) {
* showGroupCallErrorBoundary(error);
* }
* ...
* ```
* @param recoveryActionHandler
* @param onError
* @param children
* @constructor
*/
export const GroupCallErrorBoundary = ({
recoveryActionHandler,
onError,
children,
widget,
}: BoundaryProps): ReactElement => {
const fallbackRenderer: FallbackRender = useCallback(
({ error, resetError }): ReactElement => {
@@ -142,16 +123,17 @@ export const GroupCallErrorBoundary = ({
: new UnknownCallError(error instanceof Error ? error : new Error());
return (
<ErrorPage
widget={widget ?? null}
error={callError}
resetError={resetError}
recoveryActionHandler={(action: CallErrorRecoveryAction) => {
resetError();
recoveryActionHandler?.(action);
recoveryActionHandler(action);
}}
/>
);
},
[recoveryActionHandler],
[recoveryActionHandler, widget],
);
return (

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { it } from "vitest";
import { it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { type ReactElement, useCallback } from "react";
import userEvent from "@testing-library/user-event";
@@ -37,7 +37,7 @@ it("should show async error", async () => {
render(
<BrowserRouter>
<GroupCallErrorBoundaryContextProvider>
<GroupCallErrorBoundary>
<GroupCallErrorBoundary widget={null} recoveryActionHandler={vi.fn()}>
<TestComponent />
</GroupCallErrorBoundary>
</GroupCallErrorBoundaryContextProvider>

View File

@@ -15,12 +15,16 @@ import {
} from "react";
import { type MatrixClient } from "matrix-js-sdk/src/client";
import {
Room as LivekitRoom,
isE2EESupported as isE2EESupportedBrowser,
Room,
} from "livekit-client";
import { logger } from "matrix-js-sdk/src/logger";
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { JoinRule } from "matrix-js-sdk/src/matrix";
import {
MatrixRTCSessionEvent,
type MatrixRTCSession,
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
import { useNavigate } from "react-router-dom";
import type { IWidgetApiRequest } from "matrix-widget-api";
@@ -54,9 +58,16 @@ import { usePageTitle } from "../usePageTitle";
import {
E2EENotSupportedError,
ElementCallError,
ErrorCode,
RTCSessionError,
UnknownCallError,
} from "../utils/errors.ts";
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
import {
useNewMembershipManagerSetting as useNewMembershipManagerSetting,
useSetting,
} from "../settings/settings";
import { useTypedEventEmitter } from "../useEvents";
import { GroupCallErrorBoundaryContextProvider } from "./GroupCallErrorBoundaryContextProvider.tsx";
import { useGroupCallErrorBoundary } from "./useCallErrorBoundary.ts";
@@ -120,6 +131,20 @@ export const GroupCallViewInner: FC<Props> = ({
};
}, [rtcSession]);
const { showGroupCallErrorBoundary } = useGroupCallErrorBoundary();
useTypedEventEmitter(
rtcSession,
MatrixRTCSessionEvent.MembershipManagerError,
(error) => {
showGroupCallErrorBoundary(
new RTCSessionError(
ErrorCode.MEMBERSHIP_MANAGER_UNRECOVERABLE,
error.message ?? error,
),
);
},
);
useEffect(() => {
// Sanity check the room object
if (client.getRoom(rtcSession.room.roomId) !== rtcSession.room)
@@ -128,11 +153,14 @@ export const GroupCallViewInner: FC<Props> = ({
);
}, [client, rtcSession.room]);
const room = rtcSession.room as Room;
const { displayName, avatarUrl } = useProfile(client);
const roomName = useRoomName(rtcSession.room);
const roomAvatar = useRoomAvatar(rtcSession.room);
const roomName = useRoomName(room);
const roomAvatar = useRoomAvatar(room);
const { perParticipantE2EE, returnToLobby } = useUrlParams();
const e2eeSystem = useRoomEncryptionSystem(rtcSession.room.roomId);
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
usePageTitle(roomName);
const matrixInfo = useMemo((): MatrixInfo => {
@@ -140,21 +168,13 @@ export const GroupCallViewInner: FC<Props> = ({
userId: client.getUserId()!,
displayName: displayName!,
avatarUrl: avatarUrl!,
roomId: rtcSession.room.roomId,
roomId: room.roomId,
roomName,
roomAlias: rtcSession.room.getCanonicalAlias(),
roomAlias: room.getCanonicalAlias(),
roomAvatar,
e2eeSystem,
};
}, [
client,
displayName,
avatarUrl,
rtcSession.room,
roomName,
roomAvatar,
e2eeSystem,
]);
}, [client, displayName, avatarUrl, roomName, room, roomAvatar, e2eeSystem]);
// Count each member only once, regardless of how many devices they use
const participantCount = useMemo(
@@ -166,15 +186,18 @@ export const GroupCallViewInner: FC<Props> = ({
const latestDevices = useLatest(deviceContext);
const latestMuteStates = useLatest(muteStates);
const { showGroupCallErrorBoundary } = useGroupCallErrorBoundary();
const enterRTCSessionOrError = useCallback(
async (
rtcSession: MatrixRTCSession,
perParticipantE2EE: boolean,
newMembershipManager: boolean,
): Promise<void> => {
try {
await enterRTCSession(rtcSession, perParticipantE2EE);
await enterRTCSession(
rtcSession,
perParticipantE2EE,
newMembershipManager,
);
} catch (e) {
if (e instanceof ElementCallError) {
showGroupCallErrorBoundary(e);
@@ -199,7 +222,7 @@ export const GroupCallViewInner: FC<Props> = ({
// permissions and give you device names unless you specify a kind, but
// here we want all kinds of devices. This needs a fix in livekit-client
// for the following name-matching logic to do anything useful.
const devices = await Room.getLocalDevices(undefined, true);
const devices = await LivekitRoom.getLocalDevices(undefined, true);
if (audioInput) {
const deviceId = findDeviceByName(audioInput, "audioinput", devices);
@@ -239,7 +262,11 @@ export const GroupCallViewInner: FC<Props> = ({
await defaultDeviceSetup(
ev.detail.data as unknown as JoinCallData,
);
await enterRTCSessionOrError(rtcSession, perParticipantE2EE);
await enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
);
widget.api.transport.reply(ev.detail, {});
})().catch((e) => {
logger.error("Error joining RTC session", e);
@@ -252,13 +279,21 @@ export const GroupCallViewInner: FC<Props> = ({
} else {
// No lobby and no preload: we enter the rtc session right away
(async (): Promise<void> => {
await enterRTCSessionOrError(rtcSession, perParticipantE2EE);
await enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
);
})().catch((e) => {
logger.error("Error joining RTC session", e);
});
}
} else {
void enterRTCSessionOrError(rtcSession, perParticipantE2EE);
void enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
);
}
}
}, [
@@ -270,6 +305,7 @@ export const GroupCallViewInner: FC<Props> = ({
latestDevices,
latestMuteStates,
enterRTCSessionOrError,
useNewMembershipManager,
]);
const [left, setLeft] = useState(false);
@@ -287,7 +323,7 @@ export const GroupCallViewInner: FC<Props> = ({
// Otherwise the iFrame gets killed before the callEnded event got tracked.
const posthogRequest = new Promise((resolve) => {
PosthogAnalytics.instance.eventCallEnded.track(
rtcSession.room.roomId,
room.roomId,
rtcSession.memberships.length,
sendInstantly,
rtcSession,
@@ -316,11 +352,12 @@ export const GroupCallViewInner: FC<Props> = ({
});
},
[
leaveSoundContext,
widget,
rtcSession,
room.roomId,
isPasswordlessUser,
confineToRoom,
leaveSoundContext,
navigate,
],
);
@@ -346,7 +383,7 @@ export const GroupCallViewInner: FC<Props> = ({
}
}, [widget, isJoined, rtcSession]);
const joinRule = useJoinRule(rtcSession.room);
const joinRule = useJoinRule(room);
const [shareModalOpen, setInviteModalOpen] = useState(false);
const onDismissInviteModal = useCallback(
@@ -367,7 +404,7 @@ export const GroupCallViewInner: FC<Props> = ({
const shareModal = (
<InviteModal
room={rtcSession.room}
room={room}
open={shareModalOpen}
onDismiss={onDismissInviteModal}
/>
@@ -380,7 +417,11 @@ export const GroupCallViewInner: FC<Props> = ({
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() =>
void enterRTCSessionOrError(rtcSession, perParticipantE2EE)
void enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
)
}
confineToRoom={confineToRoom}
hideHeader={hideHeader}
@@ -438,9 +479,7 @@ export const GroupCallViewInner: FC<Props> = ({
}
} else if (left && widget !== null) {
// Left in widget mode:
if (!returnToLobby) {
body = null;
}
body = returnToLobby ? lobbyView : null;
} else if (preload || skipLobby) {
body = null;
} else {
@@ -449,10 +488,15 @@ export const GroupCallViewInner: FC<Props> = ({
return (
<GroupCallErrorBoundary
widget={widget}
recoveryActionHandler={(action) => {
if (action == "reconnect") {
setLeft(false);
enterRTCSessionOrError(rtcSession, perParticipantE2EE).catch((e) => {
enterRTCSessionOrError(
rtcSession,
perParticipantE2EE,
useNewMembershipManager,
).catch((e) => {
logger.error("Error re-entering RTC session", e);
});
}

View File

@@ -182,6 +182,7 @@ export const RoomPage: FC = () => {
<ErrorView
Icon={UnknownSolidIcon}
title={t("error.call_not_found")}
widget={widget}
>
<Trans i18nKey="error.call_not_found_description">
<p>
@@ -199,6 +200,7 @@ export const RoomPage: FC = () => {
<ErrorView
Icon={groupCallState.error.icon}
title={groupCallState.error.message}
widget={widget}
>
<p>{groupCallState.error.messageBody}</p>
{groupCallState.error.reason && (
@@ -212,7 +214,7 @@ export const RoomPage: FC = () => {
</FullScreenView>
);
} else {
return <ErrorPage error={groupCallState.error} />;
return <ErrorPage widget={widget} error={groupCallState.error} />;
}
default:
return <> </>;
@@ -223,7 +225,7 @@ export const RoomPage: FC = () => {
if (loading || isRegistering) {
content = <LoadingPage />;
} else if (error) {
content = <ErrorPage error={error} />;
content = <ErrorPage widget={widget} error={error} />;
} else if (!client) {
content = <RoomAuthView />;
} else if (!roomIdOrAlias) {

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`should have a reconnect button for ConnectionLostError 1`] = `
exports[`ConnectionLostError: Action handling should reset error state 1`] = `
<DocumentFragment>
<div
class="page"
@@ -150,7 +150,7 @@ exports[`should have a reconnect button for ConnectionLostError 1`] = `
</DocumentFragment>
`;
exports[`should render the error page 1`] = `
exports[`should have a close button in widget mode 1`] = `
<DocumentFragment>
<div
class="page"
@@ -287,13 +287,13 @@ exports[`should render the error page 1`] = `
The server is not configured to work with Element Call. Please contact your server admin (Domain: example.com, Error Code: MISSING_MATRIX_RTC_FOCUS).
</p>
<button
class="_button_i91xf_17 homeLink"
data-kind="tertiary"
class="_button_i91xf_17"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
Return to home screen
Close
</button>
</div>
</div>

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { expect, test, vi } from "vitest";
import { expect, onTestFinished, test, vi } from "vitest";
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
import EventEmitter from "events";
@@ -15,11 +15,17 @@ import { mockConfig } from "./utils/test";
import { ElementWidgetActions, widget } from "./widget";
import { ErrorCode } from "./utils/errors.ts";
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
vi.mock("./UrlParams", () => ({ getUrlParams }));
const actualWidget = await vi.hoisted(async () => vi.importActual("./widget"));
vi.mock("./widget", () => ({
...actualWidget,
widget: {
api: { transport: { send: vi.fn(), reply: vi.fn(), stop: vi.fn() } },
api: {
setAlwaysOnScreen: (): void => {},
transport: { send: vi.fn(), reply: vi.fn(), stop: vi.fn() },
},
lazyActions: new EventEmitter(),
},
}));
@@ -105,38 +111,50 @@ test("It joins the correct Session", async () => {
{
manageMediaKeys: false,
useLegacyMemberEvents: false,
useNewMembershipManager: true,
},
);
});
test("leaveRTCSession closes the widget on a normal hangup", async () => {
async function testLeaveRTCSession(
cause: "user" | "error",
expectClose: boolean,
): Promise<void> {
vi.clearAllMocks();
const session = { leaveRoomSession: vi.fn() } as unknown as MatrixRTCSession;
await leaveRTCSession(session, "user");
await leaveRTCSession(session, cause);
expect(session.leaveRoomSession).toHaveBeenCalled();
expect(widget!.api.transport.send).toHaveBeenCalledWith(
ElementWidgetActions.HangupCall,
expect.anything(),
);
expect(widget!.api.transport.send).toHaveBeenCalledWith(
ElementWidgetActions.Close,
expect.anything(),
);
if (expectClose) {
expect(widget!.api.transport.send).toHaveBeenCalledWith(
ElementWidgetActions.Close,
expect.anything(),
);
expect(widget!.api.transport.stop).toHaveBeenCalled();
} else {
expect(widget!.api.transport.send).not.toHaveBeenCalledWith(
ElementWidgetActions.Close,
expect.anything(),
);
expect(widget!.api.transport.stop).not.toHaveBeenCalled();
}
}
test("leaveRTCSession closes the widget on a normal hangup", async () => {
await testLeaveRTCSession("user", true);
});
test("leaveRTCSession doesn't close the widget on a fatal error", async () => {
vi.clearAllMocks();
const session = { leaveRoomSession: vi.fn() } as unknown as MatrixRTCSession;
await leaveRTCSession(session, "error");
expect(session.leaveRoomSession).toHaveBeenCalled();
expect(widget!.api.transport.send).toHaveBeenCalledWith(
ElementWidgetActions.HangupCall,
expect.anything(),
);
expect(widget!.api.transport.send).not.toHaveBeenCalledWith(
ElementWidgetActions.Close,
expect.anything(),
);
await testLeaveRTCSession("error", false);
});
test("leaveRTCSession doesn't close the widget when returning to lobby", async () => {
getUrlParams.mockReturnValue({ returnToLobby: true });
onTestFinished(() => void getUrlParams.mockReset());
await testLeaveRTCSession("user", false);
});
test("It fails with configuration error if no live kit url config is set in fallback", async () => {

View File

@@ -19,6 +19,7 @@ import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
import { Config } from "./config/Config";
import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget";
import { MatrixRTCFocusMissingError } from "./utils/errors.ts";
import { getUrlParams } from "./UrlParams.ts";
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
@@ -96,6 +97,7 @@ async function makePreferredLivekitFoci(
export async function enterRTCSession(
rtcSession: MatrixRTCSession,
encryptMedia: boolean,
useNewMembershipManager = true,
): Promise<void> {
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
@@ -113,6 +115,7 @@ export async function enterRTCSession(
await makePreferredLivekitFoci(rtcSession, livekitAlias),
makeActiveFocus(),
{
useNewMembershipManager,
manageMediaKeys: encryptMedia,
...(useDeviceSessionMemberEvents !== undefined && {
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
@@ -124,6 +127,13 @@ export async function enterRTCSession(
makeKeyDelay: matrixRtcSessionConfig?.key_rotation_on_leave_delay,
},
);
if (widget) {
try {
await widget.api.transport.send(ElementWidgetActions.JoinCall, {});
} catch (e) {
logger.error("Failed to send join action", e);
}
}
}
const widgetPostHangupProcedure = async (
@@ -149,7 +159,7 @@ const widgetPostHangupProcedure = async (
}
// On a normal user hangup we can shut down and close the widget. But if an
// error occurs we should keep the widget open until the user reads it.
if (cause === "user") {
if (cause === "user" && !getUrlParams().returnToLobby) {
try {
await widget.api.transport.send(ElementWidgetActions.Close, {});
} catch (e) {

View File

@@ -15,6 +15,7 @@ import {
debugTileLayout as debugTileLayoutSetting,
showNonMemberTiles as showNonMemberTilesSetting,
showConnectionStats as showConnectionStatsSetting,
useNewMembershipManagerSetting,
} from "./settings";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { Room as LivekitRoom } from "livekit-client";
@@ -38,6 +39,10 @@ export const DeveloperSettingsTab: FC<Props> = ({ client, livekitRoom }) => {
showConnectionStatsSetting,
);
const [useNewMembershipManager, setNewMembershipManager] = useSetting(
useNewMembershipManagerSetting,
);
const sfuUrl = useMemo((): URL | null => {
if (livekitRoom?.engine.client.ws?.url) {
// strip the URL params
@@ -134,6 +139,20 @@ export const DeveloperSettingsTab: FC<Props> = ({ client, livekitRoom }) => {
)}
/>
</FieldRow>
<FieldRow>
<InputField
id="useNewMembershipManager"
type="checkbox"
label={t("developer_mode.use_new_membership_manager")}
checked={!!useNewMembershipManager}
onChange={useCallback(
(event: ChangeEvent<HTMLInputElement>): void => {
setNewMembershipManager(event.target.checked);
},
[setNewMembershipManager],
)}
/>
</FieldRow>
{livekitRoom ? (
<>
<p>

View File

@@ -113,4 +113,8 @@ export const soundEffectVolumeSetting = new Setting<number>(
0.5,
);
export const useNewMembershipManagerSetting = new Setting<boolean>(
"new-membership-manager",
true,
);
export const alwaysShowSelf = new Setting<boolean>("always-show-self", true);

View File

@@ -9,10 +9,10 @@ import { type ComponentProps, useCallback, useEffect, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import {
ClientEvent,
type Crypto,
type MatrixClient,
type MatrixEvent,
} from "matrix-js-sdk/src/matrix";
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { getLogsForReport } from "./rageshake";
import { useClient } from "../ClientContext";
@@ -34,7 +34,7 @@ const gzip = async (text: string): Promise<Blob> => {
* Collects crypto related information.
*/
async function collectCryptoInfo(
cryptoApi: Crypto.CryptoApi,
cryptoApi: CryptoApi,
body: FormData,
): Promise<void> {
body.append("crypto_version", cryptoApi.getVersion());
@@ -82,7 +82,7 @@ async function collectCryptoInfo(
*/
async function collectRecoveryInfo(
client: MatrixClient,
cryptoApi: Crypto.CryptoApi,
cryptoApi: CryptoApi,
body: FormData,
): Promise<void> {
const secretStorage = client.secretStorage;

View File

@@ -7,6 +7,8 @@ Please see LICENSE in the repository root for full details.
import { logger } from "matrix-js-sdk/src/logger";
import { isFailure } from "./utils/fetch";
type SoundDefinition = { mp3?: string; ogg: string };
export type PrefetchedSounds<S extends string> = Promise<
@@ -49,7 +51,7 @@ export async function prefetchSounds<S extends string>(
const response = await fetch(
preferredFormat === "ogg" ? ogg : (mp3 ?? ogg),
);
if (!response.ok) {
if (isFailure(response)) {
// If the sound doesn't load, it's not the end of the world. We won't play
// the sound when requested, but it's better than failing the whole application.
logger.warn(`Could not load sound ${name}, response was not okay`);

View File

@@ -496,6 +496,10 @@ export class CallViewModel extends ViewModel {
}
return displaynameMap;
}),
// It turns out that doing the disambiguation above is rather expensive on Safari (10x slower
// than on Chrome/Firefox). This means it is important that we share() the result so that we
// don't do this work more times than we need to. This is achieve through the state() operator:
this.scope.state(),
);
/**

View File

@@ -0,0 +1,40 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { afterEach, beforeAll, describe, expect, test, vi } from "vitest";
import { shouldDisambiguate } from "./displayname";
import { alice } from "./test-fixtures";
import { mockMatrixRoom } from "./test";
// Ideally these tests would be in ./displayname.test.ts but I can't figure out how to
// just spy on the removeHiddenChars() function without impacting the other tests.
// So, these tests are in this separate test file.
vi.mock("matrix-js-sdk/src/utils");
describe("shouldDisambiguate", () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
let jsUtils: typeof import("matrix-js-sdk/src/utils");
beforeAll(async () => {
jsUtils = await import("matrix-js-sdk/src/utils");
vi.spyOn(jsUtils, "removeHiddenChars").mockImplementation((str) => str);
});
afterEach(() => {
vi.clearAllMocks();
});
test("should only call removeHiddenChars once for a single displayname", () => {
const room = mockMatrixRoom({});
shouldDisambiguate(alice, [], room);
expect(jsUtils.removeHiddenChars).toHaveBeenCalledTimes(1);
for (let i = 0; i < 10; i++) {
shouldDisambiguate(alice, [], room);
}
expect(jsUtils.removeHiddenChars).toHaveBeenCalledTimes(1);
});
});

View File

@@ -7,12 +7,36 @@ Please see LICENSE in the repository root for full details.
import {
removeDirectionOverrideChars,
removeHiddenChars,
removeHiddenChars as removeHiddenCharsUncached,
} from "matrix-js-sdk/src/utils";
import type { Room } from "matrix-js-sdk/src/matrix";
import type { CallMembership } from "matrix-js-sdk/src/matrixrtc";
// Calling removeHiddenChars() can be slow on Safari, so we cache the results.
// To illustrate a simple benchmark:
// Chrome: 10,000 calls took 2.599ms
// Safari: 10,000 calls took 242ms
// See: https://github.com/element-hq/element-call/issues/3065
const removeHiddenCharsCache = new Map<string, string>();
/**
* Calls removeHiddenCharsUncached and caches the result
*/
function removeHiddenChars(str: string): string {
if (removeHiddenCharsCache.has(str)) {
return removeHiddenCharsCache.get(str)!;
}
const result = removeHiddenCharsUncached(str);
// this is naive but should be good enough for our purposes
if (removeHiddenCharsCache.size > 500) {
removeHiddenCharsCache.clear();
}
removeHiddenCharsCache.set(str, result);
return result;
}
// Borrowed from https://github.com/matrix-org/matrix-js-sdk/blob/f10deb5ef2e8f061ff005af0476034382ea128ca/src/models/room-member.ts#L409
export function shouldDisambiguate(
member: { rawDisplayName?: string; userId: string },

View File

@@ -13,6 +13,7 @@ export enum ErrorCode {
*/
MISSING_MATRIX_RTC_FOCUS = "MISSING_MATRIX_RTC_FOCUS",
CONNECTION_LOST_ERROR = "CONNECTION_LOST_ERROR",
MEMBERSHIP_MANAGER_UNRECOVERABLE = "MEMBERSHIP_MANAGER_UNRECOVERABLE",
/** LiveKit indicates that the server has hit its track limits */
INSUFFICIENT_CAPACITY_ERROR = "INSUFFICIENT_CAPACITY_ERROR",
E2EE_NOT_SUPPORTED = "E2EE_NOT_SUPPORTED",
@@ -24,6 +25,7 @@ export enum ErrorCategory {
/** Calling is not supported, server misconfigured (JWT service missing, no MSC support ...)*/
CONFIGURATION_ISSUE = "CONFIGURATION_ISSUE",
NETWORK_CONNECTIVITY = "NETWORK_CONNECTIVITY",
RTC_SESSION_FAILURE = "RTC_SESSION_FAILURE",
CLIENT_CONFIGURATION = "CLIENT_CONFIGURATION",
UNKNOWN = "UNKNOWN",
// SYSTEM_FAILURE / FEDERATION_FAILURE ..
@@ -82,6 +84,11 @@ export class ConnectionLostError extends ElementCallError {
}
}
export class RTCSessionError extends ElementCallError {
public constructor(code: ErrorCode, message: string) {
super("RTCSession Error", code, ErrorCategory.RTC_SESSION_FAILURE, message);
}
}
export class E2EENotSupportedError extends ElementCallError {
public constructor() {
super(

30
src/utils/fetch.test.ts Normal file
View File

@@ -0,0 +1,30 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { expect, describe, it } from "vitest";
import { isFailure } from "./fetch";
describe("isFailure", () => {
it("returns false for a successful response", () => {
expect(isFailure({ ok: true, url: "https://foo.com" } as Response)).toBe(
false,
);
});
it("returns true for a failed response", () => {
expect(isFailure({ ok: false, url: "https://foo.com" } as Response)).toBe(
true,
);
});
it("returns false for a file:// URL with status 0", () => {
expect(
isFailure({ ok: false, url: "file://foo", status: 0 } as Response),
).toBe(false);
});
});

25
src/utils/fetch.ts Normal file
View File

@@ -0,0 +1,25 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
/**
* Check if a fetch response is a failure in a way that works with file:// URLs
* @param response the response to check
* @returns true if the response is a failure, false otherwise
*/
export function isFailure(response: Response): boolean {
// if response says it's okay, then it's not a failure
if (response.ok) {
return false;
}
// fetch will return status === 0 for a success on a file:// URL, so we special case it
if (response.url.startsWith("file:") && response.status === 0) {
return false;
}
return true;
}

View File

@@ -71,7 +71,7 @@ async function waitForSync(client: MatrixClient): Promise<void> {
* otherwise rust crypto will throw since it is not ready to initialize a new session.
* If another client is running make sure `.logout()` is called before executing this function.
* @param clientOptions Object of options passed through to the client
* @param restore If the rust crypto should be reset before the cient initialization or
* @param restore If the rust crypto should be reset before the client initialization or
* if the initialization should try to restore the crypto state from the indexDB.
* @returns The MatrixClient instance
*/
@@ -162,7 +162,6 @@ export async function initClient(
);
}
client.setGlobalErrorOnUnknownDevices(false);
// Once startClient is called, syncs are run asynchronously.
// Also, sync completion is communicated only via events.
// So, apply the event listener *before* starting the client.

View File

@@ -9,7 +9,7 @@
"strict": true,
"noEmit": true,
"noEmitOnError": true,
"experimentalDecorators": true,
"experimentalDecorators": false,
"esModuleInterop": true,
"noUnusedLocals": true,
"moduleResolution": "bundler",

31
vite-embedded.config.js Normal file
View File

@@ -0,0 +1,31 @@
import { defineConfig, mergeConfig } from "vite";
import fullConfig from "./vite.config";
import generateFile from "vite-plugin-generate-file";
const base = "./";
// Config for embedded deployments (possibly hosted under a non-root path)
export default defineConfig((env) =>
mergeConfig(
fullConfig({ ...env, packageType: "embedded" }),
defineConfig({
base, // Use relative URLs to allow the app to be hosted under any path
publicDir: false, // Don't serve the public directory which only contains the favicon
plugins: [
generateFile([
{
type: "json",
output: "./config.json",
data: {
matrix_rtc_session: {
key_rotation_on_leave_delay: 15000,
membership_keep_alive_period: 5000,
membership_server_side_expiry_timeout: 15000,
},
},
},
]),
],
}),
),
);

View File

@@ -7,16 +7,15 @@ Please see LICENSE in the repository root for full details.
import { defineConfig, loadEnv } from "vite";
import svgrPlugin from "vite-plugin-svgr";
import htmlTemplate from "vite-plugin-html-template";
import { createHtmlPlugin } from "vite-plugin-html";
import { codecovVitePlugin } from "@codecov/vite-plugin";
import { sentryVitePlugin } from "@sentry/vite-plugin";
import react from "@vitejs/plugin-react";
import basicSsl from "@vitejs/plugin-basic-ssl";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
export default defineConfig(({ mode, packageType }) => {
const env = loadEnv(mode, process.cwd());
const plugins = [
react(),
basicSsl(),
@@ -27,9 +26,14 @@ export default defineConfig(({ mode }) => {
ref: true,
},
}),
htmlTemplate.default({
data: {
title: env.VITE_PRODUCT_NAME || "Element Call",
createHtmlPlugin({
entry: "src/main.tsx",
inject: {
data: {
brand: env.VITE_PRODUCT_NAME || "Element Call",
packageType: packageType ?? "full",
},
},
}),

369
yarn.lock
View File

@@ -1737,6 +1737,14 @@
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
"@jridgewell/source-map@^0.3.3":
version "0.3.6"
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a"
integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15", "@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
@@ -1787,10 +1795,10 @@
dependencies:
"@bufbuild/protobuf" "^1.10.0"
"@matrix-org/matrix-sdk-crypto-wasm@^12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.1.0.tgz#2aef64eab2d30c0a1ace9c0fe876f53aa2949f14"
integrity sha512-NhJFu/8FOGjnW7mDssRUzaMSwXrYOcCqgAjZyAw9KQ9unNADKEi7KoIKe7GtrG2PWtm36y2bUf+hB8vhSY6Wdw==
"@matrix-org/matrix-sdk-crypto-wasm@^14.0.1":
version "14.0.1"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-14.0.1.tgz#e258ef84bcc7889f0e7eb3a7dbecf0830a6dd606"
integrity sha512-CgLpHs6nTw5pjSsMBi9xbQnBXf2l8YhImQP9cv8nbGSCYdYjFI0FilMXffzjWV5HThpNHri/3pF20ahZtuS3VA==
"@matrix-org/olm@3.2.15":
version "3.2.15"
@@ -2589,6 +2597,14 @@
"@react-spring/shared" "~9.7.5"
"@react-spring/types" "~9.7.5"
"@rollup/pluginutils@^4.2.0":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
dependencies:
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@rollup/pluginutils@^5.1.3":
version "5.1.3"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.3.tgz#3001bf1a03f3ad24457591f2c259c8e514e0dbdf"
@@ -3444,7 +3460,7 @@ acorn-jsx@^5.3.2:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.14.0, acorn@^8.8.1:
acorn@^8.14.0, acorn@^8.8.1, acorn@^8.8.2:
version "8.14.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
@@ -3685,6 +3701,11 @@ ast-types-flow@^0.0.8:
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6"
integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==
async@^3.2.3:
version "3.2.6"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -3880,6 +3901,11 @@ bs58@^6.0.0:
dependencies:
base-x "^5.0.0"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
@@ -3934,6 +3960,14 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camel-case@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==
dependencies:
pascal-case "^3.1.2"
tslib "^2.0.3"
camelcase@^5.0.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@@ -3965,7 +3999,7 @@ chai@^5.2.0:
loupe "^3.1.0"
pathval "^2.0.0"
chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@~4.1.0:
chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@~4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@@ -4061,6 +4095,13 @@ classnames@^2.3.1, classnames@^2.5.1:
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
clean-css@^5.2.2:
version "5.3.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd"
integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==
dependencies:
source-map "~0.6.0"
clean-regexp@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7"
@@ -4135,6 +4176,11 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colorette@^2.0.16:
version "2.0.20"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
colors@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
@@ -4152,11 +4198,21 @@ commander@^12.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
commander@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
common-tags@^1.8.0:
version "1.8.2"
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6"
@@ -4167,6 +4223,16 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
connect-history-api-fallback@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
consola@^2.15.3:
version "2.15.3"
resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
content-type@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
@@ -4257,6 +4323,17 @@ css-prefers-color-scheme@^10.0.0:
resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz#ba001b99b8105b8896ca26fc38309ddb2278bd3c"
integrity sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==
css-select@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
dependencies:
boolbase "^1.0.0"
css-what "^6.0.1"
domhandler "^4.3.1"
domutils "^2.8.0"
nth-check "^2.0.1"
css-select@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
@@ -4268,7 +4345,7 @@ css-select@^5.1.0:
domutils "^3.0.1"
nth-check "^2.0.1"
css-what@^6.1.0:
css-what@^6.0.1, css-what@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
@@ -4485,6 +4562,15 @@ dom-accessibility-api@^0.6.3:
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.2.0"
entities "^2.0.0"
dom-serializer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
@@ -4494,11 +4580,18 @@ dom-serializer@^2.0.0:
domhandler "^5.0.2"
entities "^4.2.0"
domelementtype@^2.3.0:
domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
domhandler@^4.2.0, domhandler@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
dependencies:
domelementtype "^2.2.0"
domhandler@^5.0.2, domhandler@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
@@ -4506,6 +4599,15 @@ domhandler@^5.0.2, domhandler@^5.0.3:
dependencies:
domelementtype "^2.3.0"
domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
dependencies:
dom-serializer "^1.0.1"
domelementtype "^2.2.0"
domhandler "^4.2.0"
domutils@^3.0.1, domutils@^3.1.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78"
@@ -4523,7 +4625,12 @@ dot-case@^3.0.4:
no-case "^3.0.4"
tslib "^2.0.3"
dotenv@^16.3.1:
dotenv-expand@^8.0.2:
version "8.0.3"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e"
integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==
dotenv@^16.0.0, dotenv@^16.3.1:
version "16.4.7"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26"
integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==
@@ -4551,6 +4658,13 @@ easy-table@1.2.0:
optionalDependencies:
wcwidth "^1.0.1"
ejs@^3.1.6, ejs@^3.1.9:
version "3.1.10"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
dependencies:
jake "^10.8.5"
electron-to-chromium@^1.5.73:
version "1.5.109"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.109.tgz#905a573d2b4cbb31412a2de6267fb22cf45e097e"
@@ -4587,6 +4701,11 @@ ensure-posix-path@^1.1.0:
resolved "https://registry.yarnpkg.com/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz#3c62bdb19fa4681544289edb2b382adc029179ce"
integrity sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
entities@^4.2.0, entities@^4.4.0, entities@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
@@ -5046,7 +5165,7 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^2.0.2:
estree-walker@^2.0.1, estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
@@ -5083,6 +5202,17 @@ fast-fifo@^1.3.2:
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
fast-glob@^3.2.11, fast-glob@^3.3.2, fast-glob@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.8"
fast-glob@^3.2.9:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
@@ -5094,17 +5224,6 @@ fast-glob@^3.2.9:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.3.2, fast-glob@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.8"
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@@ -5134,6 +5253,13 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
filelist@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==
dependencies:
minimatch "^5.0.1"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
@@ -5200,6 +5326,15 @@ fraction.js@^4.3.7:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
fs-extra@^10.0.1:
version "10.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-extra@^11.2.0:
version "11.3.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
@@ -5368,7 +5503,7 @@ glob@^10.4.1:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
glob@^7.0.0, glob@^7.1.3, glob@~7.2.0:
glob@^7.1.3, glob@~7.2.0:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -5502,6 +5637,11 @@ hasown@^2.0.0, hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
he@1.2.0, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
heimdalljs-logger@^0.1.10, heimdalljs-logger@^0.1.7:
version "0.1.10"
resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz#90cad58aabb1590a3c7e640ddc6a4cd3a43faaf7"
@@ -5541,6 +5681,19 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
html-minifier-terser@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab"
integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==
dependencies:
camel-case "^4.1.2"
clean-css "^5.2.2"
commander "^8.3.0"
he "^1.2.0"
param-case "^3.0.4"
relateurl "^0.2.7"
terser "^5.10.0"
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
@@ -5681,11 +5834,6 @@ internal-slot@^1.1.0:
hasown "^2.0.2"
side-channel "^1.1.0"
interpret@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
is-array-buffer@^3.0.4, is-array-buffer@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280"
@@ -5990,6 +6138,16 @@ jackspeak@^3.1.2:
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
jake@^10.8.5:
version "10.9.2"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f"
integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==
dependencies:
async "^3.2.3"
chalk "^4.0.2"
filelist "^1.0.4"
minimatch "^3.1.2"
jiti@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560"
@@ -6334,13 +6492,12 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
matrix-js-sdk@^36.1.0:
version "36.1.0"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-36.1.0.tgz#3685a85c0c1adf4e2c3622bce76c11430963f23d"
integrity sha512-KNPswMSAGKDxBybJedxRpWadaRes9paxmjTCUsQT8t1Jg3ZENraAt6ynIaxh6PxazAH9D5ly6EYKHaLMLbZ1Dg==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#9f9be701e7a8e85b5f749d0104138af36b0b82bd":
version "37.1.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/9f9be701e7a8e85b5f749d0104138af36b0b82bd"
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^12.1.0"
"@matrix-org/matrix-sdk-crypto-wasm" "^14.0.1"
"@matrix-org/olm" "3.2.15"
another-json "^0.2.0"
bs58 "^6.0.0"
@@ -6381,7 +6538,7 @@ mime-db@1.52.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
mime-types@^2.1.12, mime-types@^2.1.35:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@@ -6400,6 +6557,13 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
minimatch@^8.0.2:
version "8.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229"
@@ -6474,6 +6638,14 @@ node-fetch@^2.6.7:
dependencies:
whatwg-url "^5.0.0"
node-html-parser@^5.3.3:
version "5.4.2"
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a"
integrity sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==
dependencies:
css-select "^4.2.1"
he "1.2.0"
node-releases@^2.0.19:
version "2.0.19"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
@@ -6695,6 +6867,14 @@ pako@^2.0.4:
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
param-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==
dependencies:
dot-case "^3.0.4"
tslib "^2.0.3"
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@@ -6739,6 +6919,14 @@ parse5@^7.0.0, parse5@^7.1.2, parse5@^7.2.1:
dependencies:
entities "^4.5.0"
pascal-case@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -6777,6 +6965,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pathe@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-0.2.0.tgz#30fd7bbe0a0d91f0e60bae621f5d19e9e225c339"
integrity sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==
pathe@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
@@ -6792,7 +6985,7 @@ picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0, picocolors@^1.1.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@@ -7379,13 +7572,6 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==
dependencies:
resolve "^1.1.6"
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -7485,6 +7671,11 @@ regjsparser@^0.12.0:
dependencies:
jsesc "~3.0.2"
relateurl@^0.2.7:
version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
remove-trailing-separator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -7522,7 +7713,7 @@ resolve-options@^2.0.0:
dependencies:
value-or-function "^4.0.0"
resolve@^1.1.6, resolve@^1.10.0, resolve@^1.22.4:
resolve@^1.10.0, resolve@^1.22.4:
version "1.22.8"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
@@ -7794,15 +7985,6 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shelljs@0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
dependencies:
glob "^7.0.0"
interpret "^1.0.0"
rechoir "^0.6.2"
side-channel-list@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
@@ -7888,6 +8070,19 @@ sort-keys@^5.0.0:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0, source-map@~0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
spdx-correct@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
@@ -7946,16 +8141,7 @@ streamx@^2.12.0, streamx@^2.12.5, streamx@^2.13.2, streamx@^2.14.0:
optionalDependencies:
bare-events "^2.2.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -8055,14 +8241,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -8154,6 +8333,16 @@ teex@^1.0.1:
dependencies:
streamx "^2.12.5"
terser@^5.10.0:
version "5.39.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.39.0.tgz#0e82033ed57b3ddf1f96708d123cca717d86ca3a"
integrity sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2"
commander "^2.20.0"
source-map-support "~0.5.20"
test-exclude@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.1.tgz#20b3ba4906ac20994e275bbcafd68d510264c2a2"
@@ -8637,12 +8826,33 @@ vite-node@3.0.7:
pathe "^2.0.3"
vite "^5.0.0 || ^6.0.0"
vite-plugin-html-template@^1.1.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/vite-plugin-html-template/-/vite-plugin-html-template-1.2.2.tgz#d263c18dcf5f5e54bc74894546fd0ed993191f2f"
integrity sha512-xS9w453ropnzfYAQYPRorKa6qRKOr2kH09hSKBpUr5aSfOG0Q7KhdKg6YhrqWJ+itEpA5y4awFOfxwwVskKgYw==
vite-plugin-generate-file@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/vite-plugin-generate-file/-/vite-plugin-generate-file-0.2.0.tgz#fe143a80888c7320ffe1dad42925e8b5bf2b606c"
integrity sha512-mAHqvyFWrqbYNmWmSjhfyxShEgVN9ZPSI/cRove86KEYQIJUZ74XajXpPMwIE1xll9piA5Cmhi8kt/ydQSzZ/Q==
dependencies:
shelljs "0.8.4"
ejs "^3.1.9"
js-yaml "^4.1.0"
mime-types "^2.1.35"
picocolors "^1.0.0"
vite-plugin-html@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz#661834fa09015d3fda48ba694dbaa809396f5f7a"
integrity sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==
dependencies:
"@rollup/pluginutils" "^4.2.0"
colorette "^2.0.16"
connect-history-api-fallback "^1.6.0"
consola "^2.15.3"
dotenv "^16.0.0"
dotenv-expand "^8.0.2"
ejs "^3.1.6"
fast-glob "^3.2.11"
fs-extra "^10.0.1"
html-minifier-terser "^6.1.0"
node-html-parser "^5.3.3"
pathe "^0.2.0"
vite-plugin-svgr@^4.0.0:
version "4.3.0"
@@ -8879,7 +9089,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -8897,15 +9107,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"