diff --git a/.github/workflows/blocked.yaml b/.github/workflows/blocked.yaml new file mode 100644 index 00000000..d6e592cb --- /dev/null +++ b/.github/workflows/blocked.yaml @@ -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."); diff --git a/.github/workflows/build-and-publish-docker.yaml b/.github/workflows/build-and-publish-docker.yaml index 20a291ff..2fb361ad 100644 --- a/.github/workflows/build-and-publish-docker.yaml +++ b/.github/workflows/build-and-publish-docker.yaml @@ -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 diff --git a/.github/workflows/build-element-call.yaml b/.github/workflows/build-element-call.yaml index a41605c3..14c39b6a 100644 --- a/.github/workflows/build-element-call.yaml +++ b/.github/workflows/build-element-call.yaml @@ -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 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 891a73b8..06f1e853 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -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 }} diff --git a/.github/workflows/deploy-to-netlify.yaml b/.github/workflows/deploy-to-netlify.yaml index 6d825e3c..e865432c 100644 --- a/.github/workflows/deploy-to-netlify.yaml +++ b/.github/workflows/deploy-to-netlify.yaml @@ -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 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 2a446d11..4c4ed43c 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -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: diff --git a/.gitignore b/.gitignore index 8d306b2c..7b2cd2c2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules dist dist-ssr *.local +*.bkp .idea/ public/config.json backend/synapse_tmp/* diff --git a/README.md b/README.md index b2db171d..acadb9c0 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/Federated_Setup.drawio.png b/docs/Federated_Setup.drawio.png new file mode 100644 index 00000000..6f0a4967 Binary files /dev/null and b/docs/Federated_Setup.drawio.png differ diff --git a/docs/MSC4195_setup.drawio.png b/docs/MSC4195_setup.drawio.png new file mode 100644 index 00000000..42b4d1aa Binary files /dev/null and b/docs/MSC4195_setup.drawio.png differ diff --git a/docs/README.md b/docs/README.md index 113b52c5..4866fe4b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) diff --git a/docs/SFU_selection.drawio.png b/docs/SFU_selection.drawio.png new file mode 100644 index 00000000..f2b5e8b0 Binary files /dev/null and b/docs/SFU_selection.drawio.png differ diff --git a/docs/element_call_standalone.drawio.png b/docs/element_call_standalone.drawio.png new file mode 100644 index 00000000..7667d77c Binary files /dev/null and b/docs/element_call_standalone.drawio.png differ diff --git a/docs/element_call_widget.drawio.png b/docs/element_call_widget.drawio.png new file mode 100644 index 00000000..97d5ace3 Binary files /dev/null and b/docs/element_call_widget.drawio.png differ diff --git a/docs/embedded-standalone.md b/docs/embedded-standalone.md index f798c346..328af811 100644 --- a/docs/embedded-standalone.md +++ b/docs/embedded-standalone.md @@ -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 diff --git a/docs/self-hosting.md b/docs/self-hosting.md new file mode 100644 index 00000000..26644ed6 --- /dev/null +++ b/docs/self-hosting.md @@ -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. diff --git a/public/index.html b/index.html similarity index 70% rename from public/index.html rename to index.html index bf26d8ec..f17c73c0 100644 --- a/public/index.html +++ b/index.html @@ -2,34 +2,40 @@ + + <% if (packageType === "full") { %> + <% } %> + - <%- title %> + <%- brand %> + <% if (packageType === "full") { %> - + - + + <% } %> diff --git a/knip.ts b/knip.ts index becafc2e..05bd029d 100644 --- a/knip.ts +++ b/knip.ts @@ -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 diff --git a/locales/de/app.json b/locales/de/app.json index 2277adb0..bfad5bb5 100644 --- a/locales/de/app.json +++ b/locales/de/app.json @@ -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: <1>{{ errorCode }}). Bitte den Serveradministrator kontaktieren." }, "group_call_loader": { diff --git a/locales/en/app.json b/locales/en/app.json index 30c134bf..d80a8502 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -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": { diff --git a/package.json b/package.json index 2a7209be..a34cb584 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 2675fd3a..fb259637 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -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 { diff --git a/src/App.tsx b/src/App.tsx index 62b3cccb..5b74b4dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -72,7 +72,11 @@ export const App: FC = () => { - + ( + + )} + > } /> diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 7d7542a0..0fbff564 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -351,7 +351,7 @@ export const ClientProvider: FC = ({ children }) => { }, [initClientState, onSync]); if (alreadyOpenedErr) { - return ; + return ; } return ( diff --git a/src/ErrorView.tsx b/src/ErrorView.tsx index da6165ca..8da84ba9 100644 --- a/src/ErrorView.tsx +++ b/src/ErrorView.tsx @@ -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>; @@ -35,6 +38,7 @@ interface Props { */ fatal?: boolean; children: ReactNode; + widget: WidgetHelpers | null; } export const ErrorView: FC = ({ @@ -43,6 +47,7 @@ export const ErrorView: FC = ({ rageshake, fatal, children, + widget, }) => { const { t } = useTranslation(); const { confineToRoom } = useUrlParams(); @@ -51,6 +56,46 @@ export const ErrorView: FC = ({ 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 ( + + ); + }; + + // 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 ( + + ); + } else { + return ( + + {t("return_home_button")} + + ); + } + }; + return (
@@ -63,20 +108,11 @@ export const ErrorView: FC = ({ {rageshake && ( )} - {!confineToRoom && - (fatal || location.pathname === "/" ? ( - - ) : ( - - {t("return_home_button")} - - ))} + {widget ? ( + + ) : ( + !confineToRoom && + )}
); }; diff --git a/src/FullScreenView.tsx b/src/FullScreenView.tsx index fb2d4609..c8655229 100644 --- a/src/FullScreenView.tsx +++ b/src/FullScreenView.tsx @@ -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 = ({ 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 -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 ) : ( - +

{t("error.generic_description")}

)} diff --git a/src/RichError.tsx b/src/RichError.tsx index abacf0b3..699486e2 100644 --- a/src/RichError.tsx +++ b/src/RichError.tsx @@ -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 ( - +

{t("error.open_elsewhere_description", { brand: import.meta.env.VITE_PRODUCT_NAME || "Element Call", diff --git a/src/UrlParams.test.ts b/src/UrlParams.test.ts index 8e185abc..dce46754 100644 --- a/src/UrlParams.test.ts +++ b/src/UrlParams.test.ts @@ -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", () => { diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 61b777c7..fda4a95f 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -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, diff --git a/src/config/Config.ts b/src/config/Config.ts index d98bd9ec..b52acc46 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -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; } -async function downloadConfig( - configJsonFilename: string, -): Promise { - const url = new URL(configJsonFilename, window.location.href); - const res = await fetch(url); +async function downloadConfig(fetchTarget: string): Promise { + 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(); } diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 49f3d2ee..62a74829 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -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; diff --git a/src/home/HomePage.tsx b/src/home/HomePage.tsx index e6136855..ca1f0ea8 100644 --- a/src/home/HomePage.tsx +++ b/src/home/HomePage.tsx @@ -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 ; } else if (clientState.state === "error") { - return ; + return ; } else { return clientState.authenticated ? ( diff --git a/src/initializer.tsx b/src/initializer.tsx index 614c14dc..0879490b 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -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}`); } diff --git a/src/livekit/useECConnectionState.test.tsx b/src/livekit/useECConnectionState.test.tsx index 6ee63c3b..287ee4dc 100644 --- a/src/livekit/useECConnectionState.test.tsx +++ b/src/livekit/useECConnectionState.test.tsx @@ -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( - + , diff --git a/src/main.tsx b/src/main.tsx index fb29cf66..f6200e0e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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); }); diff --git a/src/room/GroupCallErrorBoundary.test.tsx b/src/room/GroupCallErrorBoundary.test.tsx index c42e3ded..f2a10bc2 100644 --- a/src/room/GroupCallErrorBoundary.test.tsx +++ b/src/room/GroupCallErrorBoundary.test.tsx @@ -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( - + , @@ -88,14 +93,18 @@ test("should render the error page with link back to home", async () => { const onErrorMock = vi.fn(); const { asFragment } = render( - + , ); 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( - - - - - , - ); - - 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

HELLO
; }; + 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 ( - + ); }; - render(); + const { asFragment } = render(); // 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( - + , @@ -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
Hello
; + 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( - - - - - + + + , ); - 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(); }); diff --git a/src/room/GroupCallErrorBoundary.tsx b/src/room/GroupCallErrorBoundary.tsx index a85bee9d..170718d6 100644 --- a/src/room/GroupCallErrorBoundary.tsx +++ b/src/room/GroupCallErrorBoundary.tsx @@ -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 = ({ error, recoveryActionHandler, + widget, }: ErrorPageProps): ReactElement => { const { t } = useTranslation(); - // let title: string; let icon: ComponentType>; switch (error.category) { case ErrorCategory.CONFIGURATION_ISSUE: @@ -68,7 +70,7 @@ const ErrorPage: FC = ({ 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 = ({ Icon={icon} title={error.localisedTitle} rageshake={error.code == ErrorCode.UNKNOWN_ERROR} + widget={widget} >

{error.localisedMessage ?? ( @@ -101,38 +104,16 @@ const ErrorPage: FC = ({ 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 ( { resetError(); - recoveryActionHandler?.(action); + recoveryActionHandler(action); }} /> ); }, - [recoveryActionHandler], + [recoveryActionHandler, widget], ); return ( diff --git a/src/room/GroupCallErrorBoundaryContextProvider.test.tsx b/src/room/GroupCallErrorBoundaryContextProvider.test.tsx index 128e6ae8..116aa7db 100644 --- a/src/room/GroupCallErrorBoundaryContextProvider.test.tsx +++ b/src/room/GroupCallErrorBoundaryContextProvider.test.tsx @@ -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( - + diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 3adffba5..fad91065 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -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 = ({ }; }, [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 = ({ ); }, [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 = ({ 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 = ({ const latestDevices = useLatest(deviceContext); const latestMuteStates = useLatest(muteStates); - const { showGroupCallErrorBoundary } = useGroupCallErrorBoundary(); - const enterRTCSessionOrError = useCallback( async ( rtcSession: MatrixRTCSession, perParticipantE2EE: boolean, + newMembershipManager: boolean, ): Promise => { 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 = ({ // 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 = ({ 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 = ({ } else { // No lobby and no preload: we enter the rtc session right away (async (): Promise => { - 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 = ({ latestDevices, latestMuteStates, enterRTCSessionOrError, + useNewMembershipManager, ]); const [left, setLeft] = useState(false); @@ -287,7 +323,7 @@ export const GroupCallViewInner: FC = ({ // 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 = ({ }); }, [ + leaveSoundContext, widget, rtcSession, + room.roomId, isPasswordlessUser, confineToRoom, - leaveSoundContext, navigate, ], ); @@ -346,7 +383,7 @@ export const GroupCallViewInner: FC = ({ } }, [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 = ({ const shareModal = ( @@ -380,7 +417,11 @@ export const GroupCallViewInner: FC = ({ 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 = ({ } } 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 = ({ return ( { if (action == "reconnect") { setLeft(false); - enterRTCSessionOrError(rtcSession, perParticipantE2EE).catch((e) => { + enterRTCSessionOrError( + rtcSession, + perParticipantE2EE, + useNewMembershipManager, + ).catch((e) => { logger.error("Error re-entering RTC session", e); }); } diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index 59e56aac..e96790c5 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -182,6 +182,7 @@ export const RoomPage: FC = () => {

@@ -199,6 +200,7 @@ export const RoomPage: FC = () => {

{groupCallState.error.messageBody}

{groupCallState.error.reason && ( @@ -212,7 +214,7 @@ export const RoomPage: FC = () => { ); } else { - return ; + return ; } default: return <> ; @@ -223,7 +225,7 @@ export const RoomPage: FC = () => { if (loading || isRegistering) { content = ; } else if (error) { - content = ; + content = ; } else if (!client) { content = ; } else if (!roomIdOrAlias) { diff --git a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap index f5814114..5aab22a2 100644 --- a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap +++ b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap @@ -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`] = `
`; -exports[`should render the error page 1`] = ` +exports[`should have a close button in widget mode 1`] = `
diff --git a/src/rtcSessionHelpers.test.ts b/src/rtcSessionHelpers.test.ts index 21ee2cd3..8d0b95d3 100644 --- a/src/rtcSessionHelpers.test.ts +++ b/src/rtcSessionHelpers.test.ts @@ -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 { 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 () => { diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index 719af998..0f43fd90 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -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 { 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) { diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index c7c88f5b..7f5e236b 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -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 = ({ 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 = ({ client, livekitRoom }) => { )} /> + + ): void => { + setNewMembershipManager(event.target.checked); + }, + [setNewMembershipManager], + )} + /> + {livekitRoom ? ( <>

diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 4ec0143f..ee43acc6 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -113,4 +113,8 @@ export const soundEffectVolumeSetting = new Setting( 0.5, ); +export const useNewMembershipManagerSetting = new Setting( + "new-membership-manager", + true, +); export const alwaysShowSelf = new Setting("always-show-self", true); diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index 58056ef3..9f38deb7 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -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 => { * Collects crypto related information. */ async function collectCryptoInfo( - cryptoApi: Crypto.CryptoApi, + cryptoApi: CryptoApi, body: FormData, ): Promise { 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 { const secretStorage = client.secretStorage; diff --git a/src/soundUtils.ts b/src/soundUtils.ts index b8bcabbd..2c259c73 100644 --- a/src/soundUtils.ts +++ b/src/soundUtils.ts @@ -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 = Promise< @@ -49,7 +51,7 @@ export async function prefetchSounds( 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`); diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index f634233e..ce104396 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -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(), ); /** diff --git a/src/utils/displayname-integration.test.ts b/src/utils/displayname-integration.test.ts new file mode 100644 index 00000000..8dd72d8c --- /dev/null +++ b/src/utils/displayname-integration.test.ts @@ -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); + }); +}); diff --git a/src/utils/displayname.ts b/src/utils/displayname.ts index 32532506..0eac045d 100644 --- a/src/utils/displayname.ts +++ b/src/utils/displayname.ts @@ -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(); + +/** + * 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 }, diff --git a/src/utils/errors.ts b/src/utils/errors.ts index e8adbca1..a3df68d0 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -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( diff --git a/src/utils/fetch.test.ts b/src/utils/fetch.test.ts new file mode 100644 index 00000000..16fddb7c --- /dev/null +++ b/src/utils/fetch.test.ts @@ -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); + }); +}); diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 00000000..ddc78ba5 --- /dev/null +++ b/src/utils/fetch.ts @@ -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; +} diff --git a/src/utils/matrix.ts b/src/utils/matrix.ts index 8c1a47e2..a7813528 100644 --- a/src/utils/matrix.ts +++ b/src/utils/matrix.ts @@ -71,7 +71,7 @@ async function waitForSync(client: MatrixClient): Promise { * 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. diff --git a/tsconfig.json b/tsconfig.json index 12814c82..a7c1714b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "strict": true, "noEmit": true, "noEmitOnError": true, - "experimentalDecorators": true, + "experimentalDecorators": false, "esModuleInterop": true, "noUnusedLocals": true, "moduleResolution": "bundler", diff --git a/vite-embedded.config.js b/vite-embedded.config.js new file mode 100644 index 00000000..a3e031b6 --- /dev/null +++ b/vite-embedded.config.js @@ -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, + }, + }, + }, + ]), + ], + }), + ), +); diff --git a/vite.config.js b/vite.config.js index 4c9871a2..05646608 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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", + }, }, }), diff --git a/yarn.lock b/yarn.lock index bbe4b1db..5d97bc70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"