diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 31dccb6a..db5f3fd9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -37,6 +37,12 @@ module.exports = { "@typescript-eslint/promise-function-async": "error", "@typescript-eslint/require-await": "error", "@typescript-eslint/await-thenable": "error", + // To help ensure that we get proper vite/rollup lazy loading (e.g. for matrix-js-sdk): + "@typescript-eslint/consistent-type-imports": [ + "error", + { fixStyle: "inline-type-imports" }, + ], + // To encourage good usage of RxJS: "rxjs/no-exposed-subjects": "error", }, settings: { diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index c85cddf5..8d4233a1 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -42,7 +42,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: ${{ inputs.docker_tags}} @@ -51,7 +51,7 @@ jobs: uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - name: Build and push Docker image - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 + uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/element-call.yaml b/.github/workflows/element-call.yaml index 7924140d..a424fb74 100644 --- a/.github/workflows/element-call.yaml +++ b/.github/workflows/element-call.yaml @@ -28,7 +28,7 @@ jobs: uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: cache: "yarn" - node-version: "lts/*" + node-version-file: ".node-version" - name: Install dependencies run: "yarn install" - name: Build diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 69493ff6..d9367626 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -12,7 +12,7 @@ jobs: uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: cache: "yarn" - node-version: "lts/*" + node-version-file: ".node-version" - name: Install dependencies run: "yarn install" - name: Prettier diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b63eb283..a1c7f232 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,7 +14,7 @@ jobs: uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: cache: "yarn" - node-version: "lts/*" + node-version-file: ".node-version" - name: Install dependencies run: "yarn install" - name: Vitest diff --git a/.github/workflows/translations-download.yaml b/.github/workflows/translations-download.yaml index 7359f781..30ce6ff9 100644 --- a/.github/workflows/translations-download.yaml +++ b/.github/workflows/translations-download.yaml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: cache: "yarn" - node-version: "lts/*" + node-version-file: ".node-version" - name: Install Deps run: "yarn install --frozen-lockfile" diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22 diff --git a/README.md b/README.md index a0af77fc..ffd73d5e 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,9 @@ work: 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. @@ -109,6 +112,10 @@ 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. +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 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 @@ -213,7 +220,7 @@ 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. Run `yarn i18n` to extract the new key and update the translation files. This - will add a skeleton entry to the `locales/en-GB/app.json` file: + will add a skeleton entry to the `locales/en/app.json` file: ```jsonc { ... @@ -221,7 +228,7 @@ To add a new translation key you can do these steps: ... } ``` -1. Update the skeleton entry in the `locales/en-GB/app.json` file with +1. Update the skeleton entry in the `locales/en/app.json` file with the English translation: ```jsonc diff --git a/backend/dev_homeserver.yaml b/backend/dev_homeserver.yaml index b41de45b..5697c32e 100644 --- a/backend/dev_homeserver.yaml +++ b/backend/dev_homeserver.yaml @@ -25,6 +25,9 @@ trusted_key_servers: 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. Must be a positive value if set. Defaults to no diff --git a/config/config_netlify_preview.json b/config/config_netlify_preview.json index de9600d4..ec1688d2 100644 --- a/config/config_netlify_preview.json +++ b/config/config_netlify_preview.json @@ -17,5 +17,9 @@ }, "rageshake": { "submit_url": "https://element.io/bugreports/submit" + }, + "sentry": { + "environment": "netlify-pr-preview", + "DSN": "https://b1e328d49be3402ba96101338989fb35@sentry.tools.element.io/41" } } diff --git a/config/httpd.conf b/config/httpd.conf index 597466e2..164a9b79 100644 --- a/config/httpd.conf +++ b/config/httpd.conf @@ -5,7 +5,7 @@ # disable cache entriely by default (apart from Etag which is accurate enough) - Header add Cache-Control "private no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" + Header add Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" CacheDisable on ExpiresActive off diff --git a/docs/url-params.md b/docs/url-params.md index c45c2610..010a4ec8 100644 --- a/docs/url-params.md +++ b/docs/url-params.md @@ -25,230 +25,38 @@ There are two formats for Element Call urls. ``` With this format the livekit alias that will be used is the ``. - All ppl connecting to this url will end up in the same unencrypted room. + All people connecting to this URL will end up in the same unencrypted room. This does not scale, is super unsecure - (ppl could end up in the same room by accident) and it also is not really + (people could end up in the same room by accident) and it also is not really possible to support encryption. - The url parameters are spit into two categories: **general** and **widget related**. -## Widget related params +## Parameters -**widgetId** -The id used by the widget. The presence of this parameter implies that element -call will not connect to a homeserver directly and instead tries to establish -postMessage communication via the `parentUrl`. - -```ts -widgetId: string | null; -``` - -**parentUrl** -The url used to send widget action postMessages. This should be the domain of -the client or the webview the widget is hosted in. (in case the widget is not -in an Iframe but in a dedicated webview we send the postMessages same webview -the widget lives in. Filtering is done in the widget so it ignores the messages -it receives from itself) - -```ts -parentUrl: string | null; -``` - -**userId** -The user's ID (only used in matryoshka mode). - -```ts -userId: string | null; -``` - -**deviceId** -The device's ID (only used in matryoshka mode). - -```ts -deviceId: string | null; -``` - -**baseUrl** -The base URL of the homeserver to use for media lookups in matryoshka mode. - -```ts -baseUrl: string | null; -``` - -### General url parameters - -**roomId** -Anything about what room we're pointed to should be from useRoomIdentifier which -parses the path and resolves alias with respect to the default server name, however -roomId is an exception as we need the room ID in embedded (matroyska) mode, and not -the room alias (or even the via params because we are not trying to join it). This -is also not validated, where it is in useRoomIdentifier(). - -```ts -roomId: string | null; -``` - -**confineToRoom** -Whether the app should keep the user confined to the current call/room. - -```ts -confineToRoom: boolean; (default: false) -``` - -**appPrompt** -Whether upon entering a room, the user should be prompted to launch the -native mobile app. (Affects only Android and iOS.) - -The app prompt must also be enabled in the config for this to take effect. - -```ts -appPrompt: boolean; (default: true) -``` - -**preload** -Whether the app should pause before joining the call until it sees an -io.element.join widget action, allowing it to be preloaded. - -```ts -preload: boolean; (default: false) -``` - -**hideHeader** -Whether to hide the room header when in a call. - -```ts -hideHeader: boolean; (default: false) -``` - -**showControls** -Whether to show the buttons to mute, screen-share, invite, hangup are shown -when in a call. - -```ts -showControls: boolean; (default: true) -``` - -**hideScreensharing** -Whether to hide the screen-sharing button. - -```ts -hideScreensharing: boolean; (default: false) -``` - -**enableE2EE** (Deprecated) -Whether to use end-to-end encryption. This is a legacy flag for the full mesh branch. -It is not used on the livekit branch and has no impact there! - -```ts -enableE2EE: boolean; (default: true) -``` - -**perParticipantE2EE** -Whether to use per participant encryption. -Keys will be exchanged over encrypted matrix room messages. - -```ts -perParticipantE2EE: boolean; (default: false) -``` - -**password** -E2EE password when using a shared secret. -(For individual sender keys in embedded mode this is not required.) - -```ts -password: string | null; -``` - -**displayName** -The display name to use for auto-registration. - -```ts -displayName: string | null; -``` - -**lang** -The BCP 47 code of the language the app should use. - -```ts -lang: string | null; -``` - -**fonts** -The font/fonts which the interface should use. -There can be multiple font url parameters: `?font=font-one&font=font-two...` - -```ts -font: string; -font: string; -... -``` - -**fontScale** -The factor by which to scale the interface's font size. - -```ts -fontScale: number | null; -``` - -**analyticsID** -The Posthog analytics ID. It is only available if the user has given consent for -sharing telemetry in element web. - -```ts -analyticsID: string | null; -``` - -**allowIceFallback** -Whether the app is allowed to use fallback STUN servers for ICE in case the -user's homeserver doesn't provide any. - -```ts -allowIceFallback: boolean; (default: false) -``` - -**skipLobby** -Setting this flag skips the lobby and brings you in the call directly. -In the widget this can be combined with preload to pass the device settings -with the join widget action. - -```ts -skipLobby: boolean; (default: false) -``` - -**returnToLobby** -Setting this flag makes element call show the lobby in widget mode after leaving -a call. -This is useful for video rooms. -If set to false, the widget will show a blank page after leaving the call. - -```ts -returnToLobby: boolean; (default: false) -``` - -**theme** -The theme to use for element call. -can be "light", "dark", "light-high-contrast" or "dark-high-contrast". -If not set element call will use the dark theme. - -```ts -theme: string | null; -``` - -**viaServers** -This defines the homeserver that is going to be used when joining a room. -It has to be set to a non default value for links to rooms -that are not on the default homeserver, -that is in use for the current user. - -```ts -viaServers: string; (default: undefined) -``` - -**homeserver** -This defines the homeserver that is going to be used when registering -a new (guest) user. -This can be user to configure a non default guest user server when -creating a spa link. - -```ts -homeserver: string; (default: undefined) -``` +| Name | Values | Required for widget | Required for SPA | Description | +| ------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. | +| `analyticsID` | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. | +| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. | +| `baseUrl` | | Yes | Not applicable | The base URL of the homeserver to use for media lookups. | +| `confineToRoom` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Keeps the user confined to the current call/room. | +| `deviceId` | Matrix device ID | Yes | Not applicable | The Matrix device ID for the widget host. | +| `displayName` | | No | No | Display name used for auto-registration. | +| `enableE2EE` (deprecated) | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Legacy flag to enable end-to-end encryption, not used in the `livekit` branch. | +| `fontScale` | A decimal number such as `0.9` | No, defaults to `1.0` | No, defaults to `1.0` | Factor by which to scale the interface's font size. | +| `fonts` | | No | No | Defines the font(s) used by the interface. Multiple font parameters can be specified: `?font=font-one&font=font-two...`. | +| `hideHeader` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the room header when in a call. | +| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. | +| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. | +| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. | +| `parentUrl` | | Yes | Not applicable | The url used to send widget action postMessages. This should be the domain of the client or the webview the widget is hosted in. (in case the widget is not in an Iframe but in a dedicated webview we send the postMessages same WebView the widget lives in. Filtering is done in the widget so it ignores the messages it receives from itself) | +| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) | +| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. | +| `preload` | `true` or `false` | No, defaults to `false` | Not applicable | Pauses app before joining a call until an `io.element.join` widget action is seen, allowing preloading. | +| `returnToLobby` | `true` or `false` | No, defaults to `false` | Not applicable | Displays the lobby in widget mode after leaving a call; shows a blank page if set to `false`. Useful for video rooms. | +| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. | +| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. | +| `skipLobby` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) | +| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. | +| `userId` | [Matrix User Identifier](https://spec.matrix.org/v1.12/appendices/#user-identifiers) | Yes | Not applicable | The Matrix user ID. | +| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. | +| `widgetId` | [MSC2774](https://github.com/matrix-org/matrix-spec-proposals/pull/2774) format widget ID | Yes | Not applicable | The id used by the widget. The presence of this parameter implies that element call will not connect to a homeserver directly and instead tries to establish postMessage communication via the `parentUrl`. | diff --git a/i18next-parser.config.ts b/i18next-parser.config.ts index 7d71d727..3acf2b5e 100644 --- a/i18next-parser.config.ts +++ b/i18next-parser.config.ts @@ -21,7 +21,7 @@ export default { }, ], }, - locales: ["en-GB"], + locales: ["en"], output: "locales/$LOCALE/$NAMESPACE.json", input: ["src/**/*.{ts,tsx}"], sort: true, diff --git a/localazy.json b/localazy.json index 2b9f713c..823e4a3e 100644 --- a/localazy.json +++ b/localazy.json @@ -7,13 +7,13 @@ "features": ["plural_postfix_us", "filter_untranslated"], "files": [ { - "pattern": "locales/en-GB/*.json", + "pattern": "locales/en/*.json", "lang": "inherited" }, { "group": "existing", "pattern": "locales/*/*.json", - "excludes": ["locales/en-GB/*.json"], + "excludes": ["locales/en/*.json"], "lang": "${autodetectLang}" } ] @@ -25,9 +25,6 @@ "output": "locales/${langLsrDash}/${file}" } ], - "includeSourceLang": "${includeSourceLang|false}", - "langAliases": { - "en": "en_GB" - } + "includeSourceLang": "${includeSourceLang|false}" } } diff --git a/locales/de/app.json b/locales/de/app.json index 7c465406..3ba4a36a 100644 --- a/locales/de/app.json +++ b/locales/de/app.json @@ -68,8 +68,13 @@ "username": "Benutzername", "video": "Video" }, - "crypto_version": "Krypto-Version:{{version}}", - "device_id": "Geräte-ID: {{id}}", + "developer_mode": { + "crypto_version": "Krypto-Version: {{version}}", + "device_id": "Geräte-ID: {{id}}", + "duplicate_tiles_label": "Anzahl zusätzlicher Kachelkopien pro Teilnehmer", + "hostname": "Hostname: {{hostname}}", + "matrix_id": "Matrix-ID: {{id}}" + }, "disconnected_banner": "Die Verbindung zum Server wurde getrennt.", "full_screen_view_description": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.", "full_screen_view_h1": "<0>Hoppla, etwas ist schiefgelaufen.", @@ -111,7 +116,6 @@ "login_auth_links_prompt": "Noch nicht registriert?", "login_subheading": "Weiter zu Element", "login_title": "Anmelden", - "matrix_id": "Matrix-ID: {{id}}", "microphone_off": "Mikrofon aus", "microphone_on": "Mikrofon an", "mute_microphone_button_label": "Mikrofon stumm schalten", @@ -143,13 +147,11 @@ "screenshare_button_label": "Bildschirm teilen", "settings": { "audio_tab": { - "effect_volume_description": "Lautstärke anpassen, mit der Reaktionen und Handmeldungen abgespielt werden", "effect_volume_label": "Lautstärke der Soundeffekte" }, "developer_settings_label": "Entwicklereinstellungen", "developer_settings_label_description": "Zeige die Entwicklereinstellungen im Einstellungsfenster.", "developer_tab_title": "Entwickler", - "duplicate_tiles_label": "Anzahl zusätzlicher Kachelkopien pro Teilnehmer", "feedback_tab_body": "Falls du auf Probleme stößt oder einfach nur eine Rückmeldung geben möchtest, sende uns bitte eine kurze Beschreibung.", "feedback_tab_description_label": "Deine Rückmeldung", "feedback_tab_h4": "Rückmeldung geben", @@ -159,16 +161,16 @@ "more_tab_title": "Mehr", "opt_in_description": "<0><1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.", "preferences_tab": { + "introduction": "Hier können zusätzliche Optionen für individuelle Anforderungen eingestellt werden", "reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet", "reactions_play_sound_label": "Reaktionstöne abspielen", "reactions_show_description": "Zeige eine Animation, wenn jemand eine Reaktion sendet.", "reactions_show_label": "Reaktionen anzeigen", - "reactions_title": "Reaktionen" + "reactions_title": "Reaktionen", + "show_hand_raised_timer_description": "Einen Timer zur Handmeldung anzeigen", + "show_hand_raised_timer_label": "Dauer der Handmeldung anzeigen" }, - "preferences_tab_body": "Hier können zusätzliche Optionen für individuelle Anforderungen eingestellt werden", "preferences_tab_h4": "Einstellungen", - "preferences_tab_show_hand_raised_timer_description": "Einen Timer zur Handmeldung anzeigen", - "preferences_tab_show_hand_raised_timer_label": "Dauer der Handmeldung anzeigen", "speaker_device_selection_label": "Lautsprecher" }, "star_rating_input_label_one": "{{count}} Stern", @@ -191,6 +193,7 @@ "expand": "Erweitern", "mute_for_me": "Für mich stumm schalten", "muted_for_me": "Für mich stumm geschaltet", - "volume": "Lautstärke" + "volume": "Lautstärke", + "waiting_for_media": "Warten auf Medien..." } } diff --git a/locales/en-GB/app.json b/locales/en/app.json similarity index 90% rename from locales/en-GB/app.json rename to locales/en/app.json index d71b8c9c..8e963ec1 100644 --- a/locales/en-GB/app.json +++ b/locales/en/app.json @@ -66,8 +66,15 @@ "username": "Username", "video": "Video" }, - "crypto_version": "Crypto version: {{version}}", - "device_id": "Device ID: {{id}}", + "developer_mode": { + "crypto_version": "Crypto version: {{version}}", + "debug_tile_layout_label": "Debug tile layout", + "device_id": "Device ID: {{id}}", + "duplicate_tiles_label": "Number of additional tile copies per participant", + "hostname": "Hostname: {{hostname}}", + "matrix_id": "Matrix ID: {{id}}", + "show_non_member_tiles": "Show tiles for non-member media" + }, "disconnected_banner": "Connectivity to the server has been lost.", "full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.", "full_screen_view_h1": "<0>Oops, something's gone wrong.", @@ -109,7 +116,6 @@ "login_auth_links_prompt": "Not registered yet?", "login_subheading": "To continue to Element", "login_title": "Login", - "matrix_id": "Matrix ID: {{id}}", "microphone_off": "Microphone off", "microphone_on": "Microphone on", "mute_microphone_button_label": "Mute microphone", @@ -141,11 +147,9 @@ "screenshare_button_label": "Share screen", "settings": { "audio_tab": { - "effect_volume_description": "Adjust the volume at which reactions and hand raised effects play", + "effect_volume_description": "Adjust the volume at which reactions and hand raised effects play.", "effect_volume_label": "Sound effect volume" }, - "developer_settings_label": "Developer Settings", - "developer_settings_label_description": "Expose developer settings in the settings window.", "developer_tab_title": "Developer", "devices": { "camera": "Camera", @@ -156,28 +160,26 @@ "speaker": "Speaker", "speaker_numbered": "Speaker {{n}}" }, - "duplicate_tiles_label": "Number of additional tile copies per participant", "feedback_tab_body": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.", "feedback_tab_description_label": "Your feedback", "feedback_tab_h4": "Submit feedback", "feedback_tab_send_logs_label": "Include debug logs", "feedback_tab_thank_you": "Thanks, we received your feedback!", "feedback_tab_title": "Feedback", - "more_tab_title": "More", "opt_in_description": "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "preferences_tab": { + "developer_mode_label": "Developer mode", + "developer_mode_label_description": "Enable developer mode and show developer settings tab.", + "introduction": "Here you can configure extra options for an improved experience.", "reactions_play_sound_description": "Play a sound effect when anyone sends a reaction into a call.", "reactions_play_sound_label": "Play reaction sounds", "reactions_show_description": "Show an animation when anyone sends a reaction.", "reactions_show_label": "Show reactions", - "reactions_title": "Reactions" - }, - "preferences_tab_body": "Here you can configure extra options for an improved experience", - "preferences_tab_h4": "Preferences", - "preferences_tab_show_hand_raised_timer_description": "Show a timer when a participant raises their hand", - "preferences_tab_show_hand_raised_timer_label": "Show hand raise duration" + "show_hand_raised_timer_description": "Show a timer when a participant raises their hand", + "show_hand_raised_timer_label": "Show hand raise duration" + } }, - "star_rating_input_label_one": "{{count}} stars", + "star_rating_input_label_one": "{{count}} star", "star_rating_input_label_other": "{{count}} stars", "start_new_call": "Start new call", "start_video_button_label": "Start video", @@ -197,6 +199,7 @@ "expand": "Expand", "mute_for_me": "Mute for me", "muted_for_me": "Muted for me", - "volume": "Volume" + "volume": "Volume", + "waiting_for_media": "Waiting for media..." } } diff --git a/locales/ro/app.json b/locales/ro/app.json new file mode 100644 index 00000000..6d0de16f --- /dev/null +++ b/locales/ro/app.json @@ -0,0 +1,195 @@ +{ + "a11y": { + "user_menu": "Meniul utilizatorului" + }, + "action": { + "close": "Închide", + "copy_link": "Copiază linkul", + "edit": "Editare", + "go": "Du-te", + "invite": "Invită", + "lower_hand": "Mâna inferioară", + "no": "No", + "pick_reaction": "Alegeți reacția", + "raise_hand": "Ridicați mâna", + "register": "Inregistrare", + "remove": "elimina", + "show_less": "Arată mai puțin", + "show_more": "Arată mai mult", + "sign_in": "Autentificare", + "sign_out": "Sign out", + "submit": "Trimiteți", + "upload_file": "Încărcați fișierul" + }, + "analytics_notice": "Prin participarea la această versiune beta, sunteți de acord cu colectarea de date anonime, pe care le folosim pentru a îmbunătăți produsul. Puteți găsi mai multe informații despre datele pe care le urmărim în Politica noastră de <2> confidențialitate și Politica noastră <6> privind cookie-urile.", + "app_selection_modal": { + "continue_in_browser": "Continuați în browser", + "open_in_app": "Deschideți în aplicație", + "text": "Sunteți gata să vă alăturați?", + "title": "Selectați aplicația" + }, + "application_opened_another_tab": "Această aplicație a fost deschisă într-o altă filă.", + "browser_media_e2ee_unsupported": "Browserul dvs. web nu acceptă criptarea media end-to-end. Browserele acceptate sunt Chrome, Safari, Firefox > = 117", + "browser_media_e2ee_unsupported_heading": "Browser incompatibil", + "call_ended_view": { + "body": "Ai fost deconectat de la apel", + "create_account_button": "Creează cont", + "create_account_prompt": "<0>De ce să nu terminați prin configurarea unei parole pentru a vă păstra contul? <1>Veți putea să vă păstrați numele și să setați un avatar pentru a fi utilizat la apelurile viitoare ", + "feedback_done": "<0>Vă mulțumim pentru feedback! ", + "feedback_prompt": "<0>Ne-ar plăcea să auzim feedback-ul dvs., astfel încât să vă putem îmbunătăți experiența. ", + "headline": "{{displayName}}, apelul tău s-a încheiat.", + "not_now_button": "Nu acum, reveniți la ecranul de pornire", + "reconnect_button": "Reconecta", + "survey_prompt": "Cum a mers?" + }, + "call_name": "Numele apelului", + "common": { + "analytics": "Analiză", + "audio": "Audio", + "avatar": "avatar", + "back": "Înapoi", + "camera": "Aparat foto", + "display_name": "Nume afișat", + "encrypted": "Criptat", + "error": "Eroare", + "home": "Acasa", + "loading": "Se încarcă...", + "microphone": "Microfon", + "next": "Urmator\n", + "options": "Opțiuni", + "password": "Parolă", + "preferences": "preferinte", + "profile": "Profil", + "reaction": "Reacție", + "reactions": "Reacții", + "settings": "Settings", + "something_went_wrong": "Ceva nu a mers bine", + "unencrypted": "Nu este criptat", + "username": "Nume utilizator", + "video": "Videoclip" + }, + "developer_mode": { + "crypto_version": "Versiunea Crypto: {{version}}", + "device_id": "ID-ul dispozitivului: {{id}}", + "duplicate_tiles_label": "Numărul de exemplare suplimentare de cartonașe per participant", + "hostname": "Numele gazdei: {{hostname}}", + "matrix_id": "ID-ul matricei: {{id}}" + }, + "disconnected_banner": "Conectivitatea la server a fost pierdută.", + "full_screen_view_description": "<0>Trimiterea jurnalelor de depanare ne va ajuta să urmărim problema. ", + "full_screen_view_h1": "<0>Hopa, ceva nu a mers bine. ", + "group_call_loader": { + "banned_body": "Ai fost interzis să ieși din cameră.", + "banned_heading": "Interzis", + "call_ended_body": "Ați fost eliminat din apel.", + "call_ended_heading": "Apel încheiat", + "failed_heading": "Nu s-a putut alătura", + "failed_text": "Apelul nu a fost găsit sau nu este accesibil.", + "knock_reject_body": "Cererea dvs. de a vă alătura a fost respinsă.", + "knock_reject_heading": "Acces refuzat", + "reason": "Motivul" + }, + "hangup_button_label": "Încheiați apelul", + "header_label": "Element Call Home", + "header_participants_label": "Participanți", + "invite_modal": { + "link_copied_toast": "Link copiat în clipboard", + "title": "Invitați la acest apel" + }, + "join_existing_call_modal": { + "join_button": "Da, alăturați-vă apelului", + "text": "Acest apel există deja, doriți să vă alăturați?", + "title": "Alăturați-vă apelului existent?" + }, + "layout_grid_label": "GRILĂ", + "layout_spotlight_label": "Spotlight", + "lobby": { + "ask_to_join": "Solicitare de participare la apel", + "join_as_guest": "Alăturați-vă ca invitat", + "join_button": "Alăturați-vă apelului", + "leave_button": "Înapoi la cele mai recente", + "waiting_for_invite": "Solicitare trimisă! În așteptarea permisiunii de a participa..." + }, + "log_in": "Autentificare", + "logging_in": "Autentificare...", + "login_auth_links": "<0>Creați un cont sau <2> accesați ca invitat ", + "login_auth_links_prompt": "Nu sunteți încă înregistrat?", + "login_subheading": "Pentru a continua la Element", + "login_title": "Logare", + "microphone_off": "Microfon oprit", + "microphone_on": "Microfon pornit", + "mute_microphone_button_label": "Dezactivați microfonul", + "qr_code": "COD QR", + "rageshake_button_error_caption": "Încearcă din nou trimiterea jurnalelor", + "rageshake_request_modal": { + "body": "Un alt utilizator al acestui apel are o problemă. Pentru a diagnostica mai bine aceste probleme, am dori să colectăm un jurnal de depanare.", + "title": "Solicitare jurnal de depanare" + }, + "rageshake_send_logs": "Trimiteți jurnale de depanare", + "rageshake_sending": "Trimiterea...", + "rageshake_sending_logs": "Trimiterea jurnalelor de depanare...", + "rageshake_sent": "Multumesc!", + "recaptcha_caption": "Acest site este protejat de reCAPTCHA și se aplică Politica de <2> confidențialitate Google și <6> Termenii și condițiile. <9>Făcând clic pe „Înregistrare”, sunteți de acord cu Acordul nostru de licențiere pentru utilizatorul <12> final (EULA) ", + "recaptcha_dismissed": "Recaptcha a fost respins", + "recaptcha_not_loaded": "Recaptcha nu a fost încărcat", + "register": { + "passwords_must_match": "Parolele trebuie să se potrivească", + "registering": "Înregistrare..." + }, + "register_auth_links": "<0>Ai deja un cont? <1><0>Conectați-vă sau <2> accesați ca invitat ", + "register_confirm_password_label": "Confirmă Parola", + "register_heading": "Creează-ți contul", + "return_home_button": "Reveniți la ecranul de pornire", + "room_auth_view_continue_button": "Continuă", + "room_auth_view_eula_caption": "Făcând clic pe „Continuați”, sunteți de acord cu Acordul nostru de licențiere pentru utilizatorul <2> final (EULA) ", + "screenshare_button_label": "Partajare ecran", + "settings": { + "audio_tab": { + "effect_volume_description": "Reglați volumul la care reacționează reacțiile și efectele ridicate de mână", + "effect_volume_label": "Volumul efectului sonor" + }, + "developer_settings_label": "Setări pentru dezvoltatori", + "developer_settings_label_description": "Expuneți setările dezvoltatorului în fereastra de setări.", + "developer_tab_title": "dezvoltator", + "feedback_tab_body": "Dacă întâmpinați probleme sau pur și simplu doriți să oferiți feedback, vă rugăm să ne trimiteți o scurtă descriere mai jos.", + "feedback_tab_description_label": "Feedback-ul tău", + "feedback_tab_h4": "Trimiteți Feedback", + "feedback_tab_send_logs_label": "Includeți jurnale de depanare", + "feedback_tab_thank_you": "Vă mulțumim, am primit feedback-ul dvs.!", + "feedback_tab_title": "Feedback", + "more_tab_title": "Mai mult", + "opt_in_description": "<0><1>Puteți retrage consimțământul debifând această casetă. Dacă sunteți în prezent la un apel, această setare va intra în vigoare la sfârșitul apelului.", + "preferences_tab": { + "introduction": "Aici puteți configura opțiuni suplimentare pentru o experiență îmbunătățită", + "reactions_play_sound_description": "Redați un efect sonor atunci când cineva trimite o reacție la un apel.", + "reactions_play_sound_label": "Redați sunete de reacție", + "reactions_show_description": "Afișați o animație atunci când cineva trimite o reacție.", + "reactions_show_label": "Afișați reacțiile", + "reactions_title": "Reacții", + "show_hand_raised_timer_description": "Afișați un cronometru atunci când un participant ridică mâna", + "show_hand_raised_timer_label": "Afișați durata ridicării mâinii" + }, + "preferences_tab_h4": "preferinte", + "speaker_device_selection_label": "vorbitor" + }, + "start_new_call": "Începe un nou apel", + "start_video_button_label": "Începeți videoclipul", + "stop_screenshare_button_label": "Partajarea ecranului", + "stop_video_button_label": "Opriți videoclipul", + "submitting": "Trimiterea...", + "switch_camera": "Comutați camera", + "unauthenticated_view_body": "Nu sunteți încă înregistrat? <2>Creați un cont ", + "unauthenticated_view_eula_caption": "Făcând clic pe „Go”, sunteți de acord cu Acordul nostru de licențiere pentru utilizatorul <2> final (EULA) ", + "unauthenticated_view_login_button": "Conectați-vă la contul dvs.", + "unmute_microphone_button_label": "Anulează microfonul", + "version": "{{productName}}Versiune: {{version}}", + "video_tile": { + "always_show": "Arată întotdeauna", + "change_fit_contain": "Se potrivește cadrului", + "collapse": "colaps", + "expand": "Extindeți", + "mute_for_me": "Mute pentru mine", + "muted_for_me": "Dezactivat pentru mine", + "volume": "VOLUM" + } +} diff --git a/package.json b/package.json index d6df23cf..4649ee9c 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,13 @@ "@codecov/vite-plugin": "^1.3.0", "@fontsource/inconsolata": "^5.1.0", "@fontsource/inter": "^5.1.0", - "@formatjs/intl-durationformat": "^0.6.1", + "@formatjs/intl-durationformat": "^0.7.0", "@formatjs/intl-segmenter": "^11.7.3", "@livekit/components-core": "^0.11.0", "@livekit/components-react": "^2.0.0", "@opentelemetry/api": "^1.4.0", "@opentelemetry/core": "^1.25.1", - "@opentelemetry/exporter-trace-otlp-http": "^0.54.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.56.0", "@opentelemetry/resources": "^1.25.1", "@opentelemetry/sdk-trace-base": "^1.25.1", "@opentelemetry/sdk-trace-web": "^1.9.1", @@ -45,6 +45,7 @@ "@sentry/react": "^8.0.0", "@sentry/vite-plugin": "^2.0.0", "@testing-library/dom": "^10.1.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", @@ -52,7 +53,7 @@ "@types/grecaptcha": "^3.0.9", "@types/jsdom": "^21.1.7", "@types/lodash-es": "^4.17.12", - "@types/node": "^20.0.0", + "@types/node": "^22.0.0", "@types/pako": "^2.0.3", "@types/qrcode": "^1.5.5", "@types/react-dom": "^18.3.0", @@ -62,7 +63,7 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@use-gesture/react": "^10.2.11", - "@vector-im/compound-design-tokens": "^1.9.1", + "@vector-im/compound-design-tokens": "^2.0.0", "@vector-im/compound-web": "^7.2.0", "@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-react": "^4.0.1", @@ -90,7 +91,7 @@ "livekit-client": "^2.5.7", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#2210255d6ffc909c574fb8ef16f92140b2fb7797", + "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.10.0", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", @@ -112,7 +113,7 @@ "typescript-eslint-language-service": "^5.0.5", "unique-names-generator": "^4.6.0", "vaul": "^1.0.0", - "vite": "^5.0.0", + "vite": "^6.0.0", "vite-plugin-compression2": "^1.3.1", "vite-plugin-html-template": "^1.1.0", "vite-plugin-svgr": "^4.0.0", diff --git a/public/.well-known/apple-app-site-association b/public/.well-known/apple-app-site-association deleted file mode 100644 index 088a1a04..00000000 --- a/public/.well-known/apple-app-site-association +++ /dev/null @@ -1,26 +0,0 @@ -{ - "applinks": { - "details": [ - { - "appIDs": [ - "7J4U792NQT.io.element.elementx", - "7J4U792NQT.io.element.elementx.nightly", - "7J4U792NQT.io.element.elementx.pr" - ], - "components": [ - { - "?": { - "no_universal_links": "?*" - }, - "exclude": true, - "comment": "Opt out of universal links" - }, - { - "/": "/*", - "comment": "Matches any URL" - } - ] - } - ] - } -} diff --git a/public/.well-known/assetlinks.json b/public/.well-known/assetlinks.json deleted file mode 100644 index 6f64bcc5..00000000 --- a/public/.well-known/assetlinks.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "relation": ["delegate_permission/common.handle_all_urls"], - "target": { - "namespace": "android_app", - "package_name": "io.element.android.x.debug", - "sha256_cert_fingerprints": [ - "B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E" - ] - } - }, - { - "relation": ["delegate_permission/common.handle_all_urls"], - "target": { - "namespace": "android_app", - "package_name": "io.element.android.x.nightly", - "sha256_cert_fingerprints": [ - "CA:D3:85:16:84:3A:05:CC:EB:00:AB:7B:D3:80:0F:01:BA:8F:E0:4B:38:86:F3:97:D8:F7:9A:1B:C4:54:E4:0F" - ] - } - }, - { - "relation": ["delegate_permission/common.handle_all_urls"], - "target": { - "namespace": "android_app", - "package_name": "io.element.android.x", - "sha256_cert_fingerprints": [ - "C6:DB:9B:9C:8C:BD:D6:5D:16:E8:EC:8C:8B:91:C8:31:B9:EF:C9:5C:BF:98:AE:41:F6:A9:D8:35:15:1A:7E:16" - ] - } - } -] diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 94a4e379..398ca4af 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import "matrix-js-sdk/src/@types/global"; import type { DurationFormat as PolyfillDurationFormat } from "@formatjs/intl-durationformat"; -import { Controls } from "../controls"; +import { type Controls } from "../controls"; declare global { interface Document { diff --git a/src/@types/i18next.d.ts b/src/@types/i18next.d.ts index 4a8830da..13210b0b 100644 --- a/src/@types/i18next.d.ts +++ b/src/@types/i18next.d.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import "i18next"; // import all namespaces (for the default language, only) -import app from "../../locales/en-GB/app.json"; +import type app from "../../locales/en/app.json"; declare module "i18next" { interface CustomTypeOptions { diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts index dc27b1ef..3ac7ef66 100644 --- a/src/@types/matrix-js-sdk.d.ts +++ b/src/@types/matrix-js-sdk.d.ts @@ -6,8 +6,8 @@ Please see LICENSE in the repository root for full details. */ import { - ElementCallReactionEventType, - ECallReactionEventContent, + type ElementCallReactionEventType, + type ECallReactionEventContent, } from "../reactions"; // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types diff --git a/src/App.tsx b/src/App.tsx index 8d841dba..288d4c9d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, Suspense, useEffect, useState } from "react"; +import { type FC, Suspense, useEffect, useState } from "react"; import { BrowserRouter as Router, Switch, @@ -13,7 +13,7 @@ import { useLocation, } from "react-router-dom"; import * as Sentry from "@sentry/react"; -import { History } from "history"; +import { type History } from "history"; import { TooltipProvider } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/Avatar.test.tsx b/src/Avatar.test.tsx new file mode 100644 index 00000000..1f3ddb04 --- /dev/null +++ b/src/Avatar.test.tsx @@ -0,0 +1,156 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { afterEach, expect, test, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; +import { type FC, type PropsWithChildren } from "react"; + +import { ClientContextProvider } from "./ClientContext"; +import { Avatar } from "./Avatar"; +import { mockMatrixRoomMember, mockRtcMembership } from "./utils/test"; + +const TestComponent: FC< + PropsWithChildren<{ client: MatrixClient; supportsThumbnails?: boolean }> +> = ({ client, children, supportsThumbnails }) => { + return ( + + {children} + + ); +}; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +test("should just render a placeholder when the user has no avatar", () => { + const client = vi.mocked({ + getAccessToken: () => "my-access-token", + mxcUrlToHttp: () => vi.fn(), + } as unknown as MatrixClient); + + vi.spyOn(client, "mxcUrlToHttp"); + const member = mockMatrixRoomMember( + mockRtcMembership("@alice:example.org", "AAAA"), + { + getMxcAvatarUrl: () => undefined, + }, + ); + const displayName = "Alice"; + render( + + + , + ); + const element = screen.getByRole("img", { name: "@alice:example.org" }); + expect(element.tagName).toEqual("SPAN"); + expect(client.mxcUrlToHttp).toBeCalledTimes(0); +}); + +test("should just render a placeholder when thumbnails are not supported", () => { + const client = vi.mocked({ + getAccessToken: () => "my-access-token", + mxcUrlToHttp: () => vi.fn(), + } as unknown as MatrixClient); + + vi.spyOn(client, "mxcUrlToHttp"); + const member = mockMatrixRoomMember( + mockRtcMembership("@alice:example.org", "AAAA"), + { + getMxcAvatarUrl: () => "mxc://example.org/alice-avatar", + }, + ); + const displayName = "Alice"; + render( + + + , + ); + const element = screen.getByRole("img", { name: "@alice:example.org" }); + expect(element.tagName).toEqual("SPAN"); + expect(client.mxcUrlToHttp).toBeCalledTimes(0); +}); + +test("should attempt to fetch authenticated media", async () => { + const expectedAuthUrl = "http://example.org/media/alice-avatar"; + const expectedObjectURL = "my-object-url"; + const accessToken = "my-access-token"; + const theBlob = new Blob([]); + + // vitest doesn't have a implementation of create/revokeObjectURL, so we need + // to delete the property. It's a bit odd, but it works. + Reflect.deleteProperty(global.window.URL, "createObjectURL"); + globalThis.URL.createObjectURL = vi.fn().mockReturnValue(expectedObjectURL); + Reflect.deleteProperty(global.window.URL, "revokeObjectURL"); + globalThis.URL.revokeObjectURL = vi.fn(); + + const fetchFn = vi.fn().mockResolvedValue({ + blob: async () => Promise.resolve(theBlob), + }); + vi.stubGlobal("fetch", fetchFn); + + const client = vi.mocked({ + getAccessToken: () => accessToken, + mxcUrlToHttp: () => vi.fn(), + } as unknown as MatrixClient); + + vi.spyOn(client, "mxcUrlToHttp").mockReturnValue(expectedAuthUrl); + const member = mockMatrixRoomMember( + mockRtcMembership("@alice:example.org", "AAAA"), + { + getMxcAvatarUrl: () => "mxc://example.org/alice-avatar", + }, + ); + const displayName = "Alice"; + render( + + + , + ); + + // Fetch is asynchronous, so wait for this to resolve. + await vi.waitUntil(() => + document.querySelector(`img[src='${expectedObjectURL}']`), + ); + + expect(client.mxcUrlToHttp).toBeCalledTimes(1); + expect(globalThis.fetch).toBeCalledWith(expectedAuthUrl, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); +}); diff --git a/src/Avatar.tsx b/src/Avatar.tsx index a0ae1483..dcdead7a 100644 --- a/src/Avatar.tsx +++ b/src/Avatar.tsx @@ -5,11 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { useMemo, FC } from "react"; +import { + useMemo, + type FC, + type CSSProperties, + useState, + useEffect, +} from "react"; import { Avatar as CompoundAvatar } from "@vector-im/compound-web"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; -import { getAvatarUrl } from "./utils/matrix"; -import { useClient } from "./ClientContext"; +import { useClientState } from "./ClientContext"; export enum Size { XS = "xs", @@ -33,6 +39,29 @@ interface Props { className?: string; src?: string; size?: Size | number; + style?: CSSProperties; +} + +export function getAvatarUrl( + client: MatrixClient, + mxcUrl: string | null, + avatarSize = 96, +): string | null { + const width = Math.floor(avatarSize * window.devicePixelRatio); + const height = Math.floor(avatarSize * window.devicePixelRatio); + // scale is more suitable for larger sizes + const resizeMethod = avatarSize <= 96 ? "crop" : "scale"; + return mxcUrl + ? client.mxcUrlToHttp( + mxcUrl, + width, + height, + resizeMethod, + false, + true, + true, + ) + : null; } export const Avatar: FC = ({ @@ -41,8 +70,10 @@ export const Avatar: FC = ({ name, src, size = Size.MD, + style, + ...props }) => { - const { client } = useClient(); + const clientState = useClientState(); const sizePx = useMemo( () => @@ -52,10 +83,50 @@ export const Avatar: FC = ({ [size], ); - const resolvedSrc = useMemo(() => { - if (!client || !src || !sizePx) return undefined; - return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src; - }, [client, src, sizePx]); + const [avatarUrl, setAvatarUrl] = useState(undefined); + + useEffect(() => { + if (clientState?.state !== "valid") { + return; + } + const { authenticated, supportedFeatures } = clientState; + const client = authenticated?.client; + + if (!client || !src || !sizePx || !supportedFeatures.thumbnails) { + return; + } + + const token = client.getAccessToken(); + if (!token) { + return; + } + const resolveSrc = getAvatarUrl(client, src, sizePx); + if (!resolveSrc) { + setAvatarUrl(undefined); + return; + } + + let objectUrl: string | undefined; + fetch(resolveSrc, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then(async (req) => req.blob()) + .then((blob) => { + objectUrl = URL.createObjectURL(blob); + setAvatarUrl(objectUrl); + }) + .catch((ex) => { + setAvatarUrl(undefined); + }); + + return (): void => { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + }; + }, [clientState, src, sizePx]); return ( = ({ id={id} name={name} size={`${sizePx}px`} - src={resolvedSrc} + src={avatarUrl} + style={style} + {...props} /> ); }; diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 8b5589d5..400784b5 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { - FC, + type FC, useCallback, useEffect, useState, @@ -18,7 +18,7 @@ import { import { useHistory } from "react-router-dom"; import { logger } from "matrix-js-sdk/src/logger"; import { useTranslation } from "react-i18next"; -import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; +import { type ISyncStateData, type SyncState } from "matrix-js-sdk/src/sync"; import { ClientEvent, type MatrixClient } from "matrix-js-sdk/src/client"; import type { WidgetApi } from "matrix-widget-api"; @@ -48,6 +48,7 @@ export type ValidClientState = { disconnected: boolean; supportedFeatures: { reactions: boolean; + thumbnails: boolean; }; setClient: (params?: SetClientParams) => void; }; @@ -71,6 +72,8 @@ export type SetClientParams = { const ClientContext = createContext(undefined); +export const ClientContextProvider = ClientContext.Provider; + export const useClientState = (): ClientState | undefined => useContext(ClientContext); @@ -253,6 +256,7 @@ export const ClientProvider: FC = ({ children }) => { const [isDisconnected, setIsDisconnected] = useState(false); const [supportsReactions, setSupportsReactions] = useState(false); + const [supportsThumbnails, setSupportsThumbnails] = useState(false); const state: ClientState | undefined = useMemo(() => { if (alreadyOpenedErr) { @@ -278,6 +282,7 @@ export const ClientProvider: FC = ({ children }) => { disconnected: isDisconnected, supportedFeatures: { reactions: supportsReactions, + thumbnails: supportsThumbnails, }, }; }, [ @@ -288,6 +293,7 @@ export const ClientProvider: FC = ({ children }) => { setClient, isDisconnected, supportsReactions, + supportsThumbnails, ]); const onSync = useCallback( @@ -313,6 +319,8 @@ export const ClientProvider: FC = ({ children }) => { } if (initClientState.widgetApi) { + // There is currently no widget API for authenticated media thumbnails. + setSupportsThumbnails(false); const reactSend = initClientState.widgetApi.hasCapability( "org.matrix.msc2762.send.event:m.reaction", ); @@ -334,6 +342,7 @@ export const ClientProvider: FC = ({ children }) => { } } else { setSupportsReactions(true); + setSupportsThumbnails(true); } return (): void => { diff --git a/src/DisconnectedBanner.tsx b/src/DisconnectedBanner.tsx index e317a5be..2fdb7b70 100644 --- a/src/DisconnectedBanner.tsx +++ b/src/DisconnectedBanner.tsx @@ -6,11 +6,11 @@ Please see LICENSE in the repository root for full details. */ import classNames from "classnames"; -import { FC, HTMLAttributes, ReactNode } from "react"; +import { type FC, type HTMLAttributes, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import styles from "./DisconnectedBanner.module.css"; -import { ValidClientState, useClientState } from "./ClientContext"; +import { type ValidClientState, useClientState } from "./ClientContext"; interface Props extends HTMLAttributes { children?: ReactNode; diff --git a/src/FullScreenView.tsx b/src/FullScreenView.tsx index ad66a3b8..e88f45de 100644 --- a/src/FullScreenView.tsx +++ b/src/FullScreenView.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, ReactNode, useCallback, useEffect } from "react"; +import { type FC, type ReactNode, useCallback, useEffect } from "react"; import { useLocation } from "react-router-dom"; import classNames from "classnames"; import { Trans, useTranslation } from "react-i18next"; diff --git a/src/Header.tsx b/src/Header.tsx index 69e77935..a4eb8fff 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -6,7 +6,12 @@ Please see LICENSE in the repository root for full details. */ import classNames from "classnames"; -import { FC, HTMLAttributes, ReactNode, forwardRef } from "react"; +import { + type FC, + type HTMLAttributes, + type ReactNode, + forwardRef, +} from "react"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Heading, Text } from "@vector-im/compound-web"; diff --git a/src/Modal.test.tsx b/src/Modal.test.tsx index 41bd7bbe..bb6fb0f7 100644 --- a/src/Modal.test.tsx +++ b/src/Modal.test.tsx @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { expect, test } from "vitest"; import { render } from "@testing-library/react"; -import { ReactNode, useState } from "react"; +import { type ReactNode, useState } from "react"; import { afterEach } from "node:test"; import userEvent from "@testing-library/user-event"; diff --git a/src/Modal.tsx b/src/Modal.tsx index 63d5c50a..14b6b68d 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, ReactNode, useCallback } from "react"; +import { type FC, type ReactNode, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Root as DialogRoot, diff --git a/src/QrCode.tsx b/src/QrCode.tsx index 8ad246e9..60946d70 100644 --- a/src/QrCode.tsx +++ b/src/QrCode.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useEffect, useState } from "react"; +import { type FC, useEffect, useState } from "react"; import { toDataURL } from "qrcode"; import classNames from "classnames"; import { t } from "i18next"; diff --git a/src/Slider.tsx b/src/Slider.tsx index a5eab56a..86141598 100644 --- a/src/Slider.tsx +++ b/src/Slider.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback } from "react"; +import { type FC, useCallback } from "react"; import { Root, Track, Range, Thumb } from "@radix-ui/react-slider"; import classNames from "classnames"; import { Tooltip } from "@vector-im/compound-web"; @@ -16,6 +16,9 @@ interface Props { className?: string; label: string; value: number; + /** + * Event handler called when the value changes during an interaction. + */ onValueChange: (value: number) => void; /** * Event handler called when the value changes at the end of an interaction. diff --git a/src/Toast.tsx b/src/Toast.tsx index 5b463f31..f16cfc04 100644 --- a/src/Toast.tsx +++ b/src/Toast.tsx @@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details. */ import { - ComponentType, - FC, - SVGAttributes, + type ComponentType, + type FC, + type SVGAttributes, useCallback, useEffect, } from "react"; diff --git a/src/TranslatedError.ts b/src/TranslatedError.ts index 0dbe675a..420556be 100644 --- a/src/TranslatedError.ts +++ b/src/TranslatedError.ts @@ -22,7 +22,7 @@ export abstract class TranslatedError extends Error { messageKey: ParseKeys, translationFn: TFunction, ) { - super(translationFn(messageKey, { lng: "en-GB" } as TOptions)); + super(translationFn(messageKey, { lng: "en" } as TOptions)); this.translatedMessage = translationFn(messageKey); } } diff --git a/src/UrlParams.test.ts b/src/UrlParams.test.ts index 2bf12a6b..10f1386b 100644 --- a/src/UrlParams.test.ts +++ b/src/UrlParams.test.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { describe, expect, it } from "vitest"; -import { getRoomIdentifierFromUrl } from "../src/UrlParams"; +import { getRoomIdentifierFromUrl, getUrlParams } from "../src/UrlParams"; const ROOM_NAME = "roomNameHere"; const ROOM_ID = "!d45f138fsd"; @@ -86,4 +86,113 @@ describe("UrlParams", () => { .roomAlias, ).toBeFalsy(); }); + + describe("preload", () => { + it("defaults to false", () => { + expect(getUrlParams().preload).toBe(false); + }); + + it("ignored in SPA mode", () => { + expect(getUrlParams("?preload=true").preload).toBe(false); + }); + + it("respected in widget mode", () => { + expect( + getUrlParams( + "?preload=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo", + ).preload, + ).toBe(true); + }); + }); + + describe("returnToLobby", () => { + it("is true in SPA mode", () => { + expect(getUrlParams("?returnToLobby=false").returnToLobby).toBe(true); + }); + + it("defaults to false in widget mode", () => { + expect( + getUrlParams("?widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo") + .returnToLobby, + ).toBe(false); + }); + + it("respected in widget mode", () => { + expect( + getUrlParams( + "?returnToLobby=true&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo", + ).returnToLobby, + ).toBe(true); + }); + }); + + describe("userId", () => { + it("is ignored in SPA mode", () => { + expect(getUrlParams("?userId=asd").userId).toBe(null); + }); + + it("is parsed in widget mode", () => { + expect( + getUrlParams( + "?userId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo", + ).userId, + ).toBe("asd"); + }); + }); + + describe("deviceId", () => { + it("is ignored in SPA mode", () => { + expect(getUrlParams("?deviceId=asd").deviceId).toBe(null); + }); + + it("is parsed in widget mode", () => { + expect( + getUrlParams( + "?deviceId=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo", + ).deviceId, + ).toBe("asd"); + }); + }); + + describe("baseUrl", () => { + it("is ignored in SPA mode", () => { + expect(getUrlParams("?baseUrl=asd").baseUrl).toBe(null); + }); + + it("is parsed in widget mode", () => { + expect( + getUrlParams( + "?baseUrl=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo", + ).baseUrl, + ).toBe("asd"); + }); + }); + + describe("viaServers", () => { + it("is ignored in widget mode", () => { + expect( + getUrlParams( + "?viaServers=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo", + ).viaServers, + ).toBe(null); + }); + + it("is parsed in SPA mode", () => { + expect(getUrlParams("?viaServers=asd").viaServers).toBe("asd"); + }); + }); + + describe("homeserver", () => { + it("is ignored in widget mode", () => { + expect( + getUrlParams( + "?homeserver=asd&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo", + ).homeserver, + ).toBe(null); + }); + + it("is parsed in SPA mode", () => { + expect(getUrlParams("?homeserver=asd").homeserver).toBe("asd"); + }); + }); }); diff --git a/src/UrlParams.ts b/src/UrlParams.ts index b4f6ca28..e0aae237 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -10,7 +10,7 @@ import { useLocation } from "react-router-dom"; import { logger } from "matrix-js-sdk/src/logger"; import { Config } from "./config/Config"; -import { EncryptionSystem } from "./e2ee/sharedKeyManagement"; +import { type EncryptionSystem } from "./e2ee/sharedKeyManagement"; import { E2eeType } from "./e2ee/e2eeType"; interface RoomIdentifier { @@ -211,9 +211,13 @@ export const getUrlParams = ( const fontScale = parseFloat(parser.getParam("fontScale") ?? ""); + const widgetId = parser.getParam("widgetId"); + const parentUrl = parser.getParam("parentUrl"); + const isWidget = !!widgetId && !!parentUrl; + return { - widgetId: parser.getParam("widgetId"), - parentUrl: parser.getParam("parentUrl"), + widgetId, + parentUrl, // NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl: // what would we do if it were invalid? If the widget API says that's what @@ -224,15 +228,15 @@ export const getUrlParams = ( confineToRoom: parser.getFlagParam("confineToRoom") || parser.getFlagParam("embed"), appPrompt: parser.getFlagParam("appPrompt", true), - preload: parser.getFlagParam("preload"), + preload: isWidget ? parser.getFlagParam("preload") : false, hideHeader: parser.getFlagParam("hideHeader"), showControls: parser.getFlagParam("showControls", true), hideScreensharing: parser.getFlagParam("hideScreensharing"), e2eEnabled: parser.getFlagParam("enableE2EE", true), - userId: parser.getParam("userId"), + userId: isWidget ? parser.getParam("userId") : null, displayName: parser.getParam("displayName"), - deviceId: parser.getParam("deviceId"), - baseUrl: parser.getParam("baseUrl"), + deviceId: isWidget ? parser.getParam("deviceId") : null, + baseUrl: isWidget ? parser.getParam("baseUrl") : null, lang: parser.getParam("lang"), fonts: parser.getAllParams("font"), fontScale: Number.isNaN(fontScale) ? null : fontScale, @@ -240,10 +244,10 @@ export const getUrlParams = ( allowIceFallback: parser.getFlagParam("allowIceFallback"), perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"), skipLobby: parser.getFlagParam("skipLobby"), - returnToLobby: parser.getFlagParam("returnToLobby"), + returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : true, theme: parser.getParam("theme"), - viaServers: parser.getParam("viaServers"), - homeserver: parser.getParam("homeserver"), + viaServers: !isWidget ? parser.getParam("viaServers") : null, + homeserver: !isWidget ? parser.getParam("homeserver") : null, }; }; diff --git a/src/UserMenu.tsx b/src/UserMenu.tsx index 906f220f..0cb9868b 100644 --- a/src/UserMenu.tsx +++ b/src/UserMenu.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useMemo, useState } from "react"; +import { type FC, useMemo, useState } from "react"; import { useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Menu, MenuItem } from "@vector-im/compound-web"; diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx index e73f2780..2695552d 100644 --- a/src/UserMenuContainer.tsx +++ b/src/UserMenuContainer.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback, useState } from "react"; +import { type FC, useCallback, useState } from "react"; import { useHistory, useLocation } from "react-router-dom"; import { useClientLegacy } from "./ClientContext"; diff --git a/src/analytics/AnalyticsNotice.tsx b/src/analytics/AnalyticsNotice.tsx index 9ba78f0d..9725d596 100644 --- a/src/analytics/AnalyticsNotice.tsx +++ b/src/analytics/AnalyticsNotice.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC } from "react"; +import { type FC } from "react"; import { Trans } from "react-i18next"; import { ExternalLink } from "../button/Link"; diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 0df0ee32..5124e711 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -5,9 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import posthog, { CaptureOptions, PostHog, Properties } from "posthog-js"; +import posthog, { + type CaptureOptions, + type PostHog, + type Properties, +} from "posthog-js"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { Buffer } from "buffer"; import { widget } from "../widget"; diff --git a/src/analytics/PosthogEvents.ts b/src/analytics/PosthogEvents.ts index ca086dc2..2e5744d2 100644 --- a/src/analytics/PosthogEvents.ts +++ b/src/analytics/PosthogEvents.ts @@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { DisconnectReason } from "livekit-client"; +import { type DisconnectReason } from "livekit-client"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; import { - IPosthogEvent, + type IPosthogEvent, PosthogAnalytics, RegistrationType, } from "./PosthogAnalytics"; diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index 102de159..c03fcab9 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details. */ import { - SpanProcessor, - ReadableSpan, - Span, + type SpanProcessor, + type ReadableSpan, + type Span, } from "@opentelemetry/sdk-trace-base"; import { hrTimeToMilliseconds } from "@opentelemetry/core"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/analytics/RageshakeSpanProcessor.ts b/src/analytics/RageshakeSpanProcessor.ts index ee547e29..df42641e 100644 --- a/src/analytics/RageshakeSpanProcessor.ts +++ b/src/analytics/RageshakeSpanProcessor.ts @@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { AttributeValue, Attributes } from "@opentelemetry/api"; +import { type AttributeValue, type Attributes } from "@opentelemetry/api"; import { hrTimeToMicroseconds } from "@opentelemetry/core"; import { - SpanProcessor, - ReadableSpan, - Span, + type SpanProcessor, + type ReadableSpan, + type Span, } from "@opentelemetry/sdk-trace-base"; const dumpAttributes = ( diff --git a/src/auth/LoginPage.tsx b/src/auth/LoginPage.tsx index e4aede09..11b831cd 100644 --- a/src/auth/LoginPage.tsx +++ b/src/auth/LoginPage.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, FormEvent, useCallback, useRef, useState } from "react"; +import { type FC, type FormEvent, useCallback, useRef, useState } from "react"; import { useHistory, useLocation } from "react-router-dom"; import { Trans, useTranslation } from "react-i18next"; import { Button } from "@vector-im/compound-web"; diff --git a/src/auth/RegisterPage.tsx b/src/auth/RegisterPage.tsx index 392f8a7a..b12ff2ca 100644 --- a/src/auth/RegisterPage.tsx +++ b/src/auth/RegisterPage.tsx @@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details. */ import { - ChangeEvent, - FC, - FormEvent, + type ChangeEvent, + type FC, + type FormEvent, useCallback, useEffect, useRef, diff --git a/src/auth/useInteractiveLogin.ts b/src/auth/useInteractiveLogin.ts index 2bd15acb..8a70dee2 100644 --- a/src/auth/useInteractiveLogin.ts +++ b/src/auth/useInteractiveLogin.ts @@ -9,12 +9,12 @@ import { useCallback } from "react"; import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth"; import { createClient, - LoginResponse, - MatrixClient, + type LoginResponse, + type MatrixClient, } from "matrix-js-sdk/src/matrix"; import { initClient } from "../utils/matrix"; -import { Session } from "../ClientContext"; +import { type Session } from "../ClientContext"; /** * This provides the login method to login using user credentials. * @param oldClient If there is an already authenticated client it should be passed to this hook diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index 2c272cb1..d6568ede 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -9,13 +9,13 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth"; import { createClient, - MatrixClient, - RegisterResponse, + type MatrixClient, + type RegisterResponse, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { initClient } from "../utils/matrix"; -import { Session } from "../ClientContext"; +import { type Session } from "../ClientContext"; import { Config } from "../config/Config"; import { widget } from "../widget"; diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 5c85ddbf..c4fb2db7 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -4,7 +4,7 @@ Copyright 2022-2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ComponentPropsWithoutRef, FC } from "react"; +import { type ComponentPropsWithoutRef, type FC } from "react"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; import { Button as CpdButton, Tooltip } from "@vector-im/compound-web"; diff --git a/src/button/InviteButton.tsx b/src/button/InviteButton.tsx index 874c1046..bbd023f5 100644 --- a/src/button/InviteButton.tsx +++ b/src/button/InviteButton.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ComponentPropsWithoutRef, FC } from "react"; +import { type ComponentPropsWithoutRef, type FC } from "react"; import { Button } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; import { UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; diff --git a/src/button/Link.tsx b/src/button/Link.tsx index 68c4dd13..829cbdc8 100644 --- a/src/button/Link.tsx +++ b/src/button/Link.tsx @@ -6,15 +6,15 @@ Please see LICENSE in the repository root for full details. */ import { - ComponentPropsWithoutRef, + type ComponentPropsWithoutRef, forwardRef, - MouseEvent, + type MouseEvent, useCallback, useMemo, } from "react"; import { Link as CpdLink } from "@vector-im/compound-web"; import { useHistory } from "react-router-dom"; -import { createPath, LocationDescriptor, Path } from "history"; +import { createPath, type LocationDescriptor, type Path } from "history"; import classNames from "classnames"; import { useLatest } from "../useLatest"; diff --git a/src/button/LinkButton.tsx b/src/button/LinkButton.tsx index 5e231a49..b0e733b0 100644 --- a/src/button/LinkButton.tsx +++ b/src/button/LinkButton.tsx @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ComponentPropsWithoutRef, forwardRef } from "react"; +import { type ComponentPropsWithoutRef, forwardRef } from "react"; import { Button } from "@vector-im/compound-web"; -import { LocationDescriptor } from "history"; +import { type LocationDescriptor } from "history"; import { useLink } from "./Link"; diff --git a/src/button/ReactionToggleButton.test.tsx b/src/button/ReactionToggleButton.test.tsx index a1498304..72437825 100644 --- a/src/button/ReactionToggleButton.test.tsx +++ b/src/button/ReactionToggleButton.test.tsx @@ -9,7 +9,7 @@ import { render } from "@testing-library/react"; import { expect, test } from "vitest"; import { TooltipProvider } from "@vector-im/compound-web"; import { userEvent } from "@testing-library/user-event"; -import { ReactNode } from "react"; +import { type ReactNode } from "react"; import { MockRoom, diff --git a/src/button/ReactionToggleButton.tsx b/src/button/ReactionToggleButton.tsx index b1d6ec3e..7f231d30 100644 --- a/src/button/ReactionToggleButton.tsx +++ b/src/button/ReactionToggleButton.tsx @@ -13,9 +13,9 @@ import { ReactionSolidIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { - ComponentPropsWithoutRef, - FC, - ReactNode, + type ComponentPropsWithoutRef, + type FC, + type ReactNode, useCallback, useEffect, useMemo, @@ -27,7 +27,11 @@ import classNames from "classnames"; import { useReactions } from "../useReactions"; import styles from "./ReactionToggleButton.module.css"; -import { ReactionOption, ReactionSet, ReactionsRowSize } from "../reactions"; +import { + type ReactionOption, + ReactionSet, + ReactionsRowSize, +} from "../reactions"; import { Modal } from "../Modal"; interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> { diff --git a/src/config/Config.ts b/src/config/Config.ts index e3c43bf1..27a8cb7f 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -10,8 +10,8 @@ import { merge } from "lodash-es"; import { getUrlParams } from "../UrlParams"; import { DEFAULT_CONFIG, - ConfigOptions, - ResolvedConfigOptions, + type ConfigOptions, + type ResolvedConfigOptions, } from "./ConfigOptions"; export class Config { diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index ed4d5bce..3947ba66 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -51,7 +51,7 @@ export interface ConfigOptions { // a livekit service url in the client well-known. // The well known needs to be formatted like so: // {"type":"livekit", "livekit_service_url":"https://livekit.example.com"} - // and stored under the key: "livekit_focus" + // and stored under the key: "org.matrix.msc4143.rtc_foci" livekit_service_url: string; }; diff --git a/src/e2ee/matrixKeyProvider.test.ts b/src/e2ee/matrixKeyProvider.test.ts index e5b4015f..df4c6009 100644 --- a/src/e2ee/matrixKeyProvider.test.ts +++ b/src/e2ee/matrixKeyProvider.test.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { describe, expect, test, vi } from "vitest"; import { - MatrixRTCSession, + type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/src/matrixrtc"; import { KeyProviderEvent } from "livekit-client"; diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index 6cbecd19..2d269bae 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { BaseKeyProvider, createKeyMaterialFromBuffer } from "livekit-client"; import { logger } from "matrix-js-sdk/src/logger"; import { - MatrixRTCSession, + type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; diff --git a/src/e2ee/sharedKeyManagement.ts b/src/e2ee/sharedKeyManagement.ts index 2b67ec4a..7936de8b 100644 --- a/src/e2ee/sharedKeyManagement.ts +++ b/src/e2ee/sharedKeyManagement.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { useEffect, useMemo } from "react"; import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage"; -import { UrlParams, getUrlParams, useUrlParams } from "../UrlParams"; +import { type UrlParams, getUrlParams, useUrlParams } from "../UrlParams"; import { E2eeType } from "./e2eeType"; import { useClient } from "../ClientContext"; diff --git a/src/form/Form.tsx b/src/form/Form.tsx index 03291b79..49cd1b65 100644 --- a/src/form/Form.tsx +++ b/src/form/Form.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import classNames from "classnames"; -import { FormEventHandler, forwardRef, ReactNode } from "react"; +import { type FormEventHandler, forwardRef, type ReactNode } from "react"; import styles from "./Form.module.css"; diff --git a/src/grid/CallLayout.ts b/src/grid/CallLayout.ts index 895af23f..e05cd025 100644 --- a/src/grid/CallLayout.ts +++ b/src/grid/CallLayout.ts @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { BehaviorSubject, Observable } from "rxjs"; -import { ComponentType } from "react"; +import { type BehaviorSubject, type Observable } from "rxjs"; +import { type ComponentType } from "react"; -import { LayoutProps } from "./Grid"; -import { TileViewModel } from "../state/TileViewModel"; +import { type LayoutProps } from "./Grid"; +import { type TileViewModel } from "../state/TileViewModel"; export interface Bounds { width: number; diff --git a/src/grid/Grid.tsx b/src/grid/Grid.tsx index 2983357b..268d4352 100644 --- a/src/grid/Grid.tsx +++ b/src/grid/Grid.tsx @@ -6,25 +6,24 @@ Please see LICENSE in the repository root for full details. */ import { - SpringRef, - TransitionFn, - animated, + type SpringRef, + type TransitionFn, + type animated, useTransition, } from "@react-spring/web"; -import { EventTypes, Handler, useScroll } from "@use-gesture/react"; +import { type EventTypes, type Handler, useScroll } from "@use-gesture/react"; import { - CSSProperties, - ComponentProps, - ComponentType, - Dispatch, - FC, - LegacyRef, - ReactNode, - SetStateAction, + type CSSProperties, + type ComponentProps, + type ComponentType, + type Dispatch, + type FC, + type LegacyRef, + type ReactNode, + type SetStateAction, createContext, forwardRef, memo, - useCallback, useContext, useEffect, useMemo, @@ -54,7 +53,6 @@ interface Tile { id: string; model: Model; onDrag: DragCallback | undefined; - setVisible: (visible: boolean) => void; } type PlacedTile = Tile & Rect; @@ -88,7 +86,6 @@ interface SlotProps extends Omit, "onDrag"> { id: string; model: Model; onDrag?: DragCallback; - onVisibilityChange?: (visible: boolean) => void; style?: CSSProperties; className?: string; } @@ -115,24 +112,47 @@ function offset(element: HTMLElement, relativeTo: Element): Offset { } } +export type VisibleTilesCallback = (visibleTiles: number) => void; + interface LayoutContext { setGeneration: Dispatch>; + setVisibleTilesCallback: Dispatch< + SetStateAction + >; } const LayoutContext = createContext(null); +function useLayoutContext(): LayoutContext { + const context = useContext(LayoutContext); + if (context === null) + throw new Error("useUpdateLayout called outside a Grid layout context"); + return context; +} + /** * Enables Grid to react to layout changes. You must call this in your Layout * component or else Grid will not be reactive. */ export function useUpdateLayout(): void { - const context = useContext(LayoutContext); - if (context === null) - throw new Error("useUpdateLayout called outside a Grid layout context"); - + const { setGeneration } = useLayoutContext(); // On every render, tell Grid that the layout may have changed - useEffect(() => - context.setGeneration((prev) => (prev === null ? 0 : prev + 1)), + useEffect(() => setGeneration((prev) => (prev === null ? 0 : prev + 1))); +} + +/** + * Asks Grid to call a callback whenever the number of visible tiles may have + * changed. + */ +export function useVisibleTiles(callback: VisibleTilesCallback): void { + const { setVisibleTilesCallback } = useLayoutContext(); + useEffect( + () => setVisibleTilesCallback(() => callback), + [callback, setVisibleTilesCallback], + ); + useEffect( + () => (): void => setVisibleTilesCallback(null), + [setVisibleTilesCallback], ); } @@ -245,39 +265,20 @@ export function Grid< const windowHeight = useObservableEagerState(windowHeightObservable); const [layoutRoot, setLayoutRoot] = useState(null); const [generation, setGeneration] = useState(null); + const [visibleTilesCallback, setVisibleTilesCallback] = + useState(null); const tiles = useInitial(() => new Map>()); const prefersReducedMotion = usePrefersReducedMotion(); const Slot: FC> = useMemo( () => - function Slot({ - id, - model, - onDrag, - onVisibilityChange, - style, - className, - ...props - }) { + function Slot({ id, model, onDrag, style, className, ...props }) { const ref = useRef(null); - const prevVisible = useRef(null); - const setVisible = useCallback( - (visible: boolean) => { - if ( - onVisibilityChange !== undefined && - visible !== prevVisible.current - ) { - onVisibilityChange(visible); - prevVisible.current = visible; - } - }, - [onVisibilityChange], - ); useEffect(() => { - tiles.set(id, { id, model, onDrag, setVisible }); + tiles.set(id, { id, model, onDrag }); return (): void => void tiles.delete(id); - }, [id, model, onDrag, setVisible]); + }, [id, model, onDrag]); return (
({ setGeneration }), []); + const context: LayoutContext = useMemo( + () => ({ setGeneration, setVisibleTilesCallback }), + [setVisibleTilesCallback], + ); // Combine the tile definitions and slots together to create placed tiles const placedTiles = useMemo(() => { @@ -342,9 +346,11 @@ export function Grid< ); useEffect(() => { - for (const tile of placedTiles) - tile.setVisible(tile.y + tile.height <= visibleHeight); - }, [placedTiles, visibleHeight]); + visibleTilesCallback?.( + placedTiles.filter((tile) => tile.y + tile.height <= visibleHeight) + .length, + ); + }, [placedTiles, visibleTilesCallback, visibleHeight]); // Drag state is stored in a ref rather than component state, because we use // react-spring's imperative API during gestures to improve responsiveness diff --git a/src/grid/GridLayout.tsx b/src/grid/GridLayout.tsx index 3dc3bef1..f4a29379 100644 --- a/src/grid/GridLayout.tsx +++ b/src/grid/GridLayout.tsx @@ -5,15 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { CSSProperties, forwardRef, useCallback, useMemo } from "react"; +import { type CSSProperties, forwardRef, useCallback, useMemo } from "react"; import { distinctUntilChanged } from "rxjs"; import { useObservableEagerState } from "observable-hooks"; -import { GridLayout as GridLayoutModel } from "../state/CallViewModel"; +import { type GridLayout as GridLayoutModel } from "../state/CallViewModel"; import styles from "./GridLayout.module.css"; import { useInitial } from "../useInitial"; -import { CallLayout, arrangeTiles } from "./CallLayout"; -import { DragCallback, useUpdateLayout } from "./Grid"; +import { type CallLayout, arrangeTiles } from "./CallLayout"; +import { type DragCallback, useUpdateLayout, useVisibleTiles } from "./Grid"; interface GridCSSProperties extends CSSProperties { "--gap": string; @@ -73,6 +73,7 @@ export const makeGridLayout: CallLayout = ({ // The scrolling part of the layout is where all the grid tiles live scrolling: forwardRef(function GridLayout({ model, Slot }, ref) { useUpdateLayout(); + useVisibleTiles(model.setVisibleTiles); const { width, height: minHeight } = useObservableEagerState(minBounds); const { gap, tileWidth, tileHeight } = useMemo( () => arrangeTiles(width, minHeight, model.grid.length), @@ -93,13 +94,7 @@ export const makeGridLayout: CallLayout = ({ } > {model.grid.map((m) => ( - + ))}
); diff --git a/src/grid/OneOnOneLayout.tsx b/src/grid/OneOnOneLayout.tsx index 03ff5b32..fb0af714 100644 --- a/src/grid/OneOnOneLayout.tsx +++ b/src/grid/OneOnOneLayout.tsx @@ -9,10 +9,10 @@ import { forwardRef, useCallback, useMemo } from "react"; import { useObservableEagerState } from "observable-hooks"; import classNames from "classnames"; -import { OneOnOneLayout as OneOnOneLayoutModel } from "../state/CallViewModel"; -import { CallLayout, arrangeTiles } from "./CallLayout"; +import { type OneOnOneLayout as OneOnOneLayoutModel } from "../state/CallViewModel"; +import { type CallLayout, arrangeTiles } from "./CallLayout"; import styles from "./OneOnOneLayout.module.css"; -import { DragCallback, useUpdateLayout } from "./Grid"; +import { type DragCallback, useUpdateLayout } from "./Grid"; /** * An implementation of the "one-on-one" layout, in which the remote participant @@ -52,7 +52,6 @@ export const makeOneOnOneLayout: CallLayout = ({ @@ -61,7 +60,6 @@ export const makeOneOnOneLayout: CallLayout = ({ id={model.local.id} model={model.local} onDrag={onDragLocalTile} - onVisibilityChange={model.local.setVisible} data-block-alignment={pipAlignmentValue.block} data-inline-alignment={pipAlignmentValue.inline} /> diff --git a/src/grid/SpotlightExpandedLayout.tsx b/src/grid/SpotlightExpandedLayout.tsx index 08495036..d4ce9af3 100644 --- a/src/grid/SpotlightExpandedLayout.tsx +++ b/src/grid/SpotlightExpandedLayout.tsx @@ -8,9 +8,9 @@ Please see LICENSE in the repository root for full details. import { forwardRef, useCallback } from "react"; import { useObservableEagerState } from "observable-hooks"; -import { SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel"; -import { CallLayout } from "./CallLayout"; -import { DragCallback, useUpdateLayout } from "./Grid"; +import { type SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel"; +import { type CallLayout } from "./CallLayout"; +import { type DragCallback, useUpdateLayout } from "./Grid"; import styles from "./SpotlightExpandedLayout.module.css"; /** @@ -63,7 +63,6 @@ export const makeSpotlightExpandedLayout: CallLayout< id={model.pip.id} model={model.pip} onDrag={onDragPip} - onVisibilityChange={model.pip.setVisible} data-block-alignment={pipAlignmentValue.block} data-inline-alignment={pipAlignmentValue.inline} /> diff --git a/src/grid/SpotlightLandscapeLayout.tsx b/src/grid/SpotlightLandscapeLayout.tsx index b9e6b289..3b80a166 100644 --- a/src/grid/SpotlightLandscapeLayout.tsx +++ b/src/grid/SpotlightLandscapeLayout.tsx @@ -9,10 +9,10 @@ import { forwardRef } from "react"; import { useObservableEagerState } from "observable-hooks"; import classNames from "classnames"; -import { CallLayout } from "./CallLayout"; -import { SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel"; +import { type CallLayout } from "./CallLayout"; +import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel"; import styles from "./SpotlightLandscapeLayout.module.css"; -import { useUpdateLayout } from "./Grid"; +import { useUpdateLayout, useVisibleTiles } from "./Grid"; /** * An implementation of the "spotlight landscape" layout, in which the spotlight @@ -50,6 +50,7 @@ export const makeSpotlightLandscapeLayout: CallLayout< ref, ) { useUpdateLayout(); + useVisibleTiles(model.setVisibleTiles); useObservableEagerState(minBounds); const withIndicators = useObservableEagerState(model.spotlight.media).length > 1; @@ -63,13 +64,7 @@ export const makeSpotlightLandscapeLayout: CallLayout< />
{model.grid.map((m) => ( - + ))}
diff --git a/src/grid/SpotlightPortraitLayout.tsx b/src/grid/SpotlightPortraitLayout.tsx index e617160e..6af0fa39 100644 --- a/src/grid/SpotlightPortraitLayout.tsx +++ b/src/grid/SpotlightPortraitLayout.tsx @@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { CSSProperties, forwardRef } from "react"; +import { type CSSProperties, forwardRef } from "react"; import { useObservableEagerState } from "observable-hooks"; import classNames from "classnames"; -import { CallLayout, arrangeTiles } from "./CallLayout"; -import { SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel"; +import { type CallLayout, arrangeTiles } from "./CallLayout"; +import { type SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel"; import styles from "./SpotlightPortraitLayout.module.css"; -import { useUpdateLayout } from "./Grid"; +import { useUpdateLayout, useVisibleTiles } from "./Grid"; interface GridCSSProperties extends CSSProperties { "--grid-gap": string; @@ -54,6 +54,7 @@ export const makeSpotlightPortraitLayout: CallLayout< ref, ) { useUpdateLayout(); + useVisibleTiles(model.setVisibleTiles); const { width } = useObservableEagerState(minBounds); const { gap, tileWidth, tileHeight } = arrangeTiles( width, @@ -84,13 +85,7 @@ export const makeSpotlightPortraitLayout: CallLayout< />
{model.grid.map((m) => ( - + ))}
diff --git a/src/grid/TileWrapper.tsx b/src/grid/TileWrapper.tsx index aeb581fe..a2eebd43 100644 --- a/src/grid/TileWrapper.tsx +++ b/src/grid/TileWrapper.tsx @@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ComponentType, memo, RefObject, useRef } from "react"; -import { EventTypes, Handler, useDrag } from "@use-gesture/react"; -import { SpringValue } from "@react-spring/web"; +import { type ComponentType, memo, type RefObject, useRef } from "react"; +import { type EventTypes, type Handler, useDrag } from "@use-gesture/react"; +import { type SpringValue } from "@react-spring/web"; import classNames from "classnames"; -import { TileProps } from "./Grid"; +import { type TileProps } from "./Grid"; import styles from "./TileWrapper.module.css"; interface Props { diff --git a/src/home/CallList.test.tsx b/src/home/CallList.test.tsx index cd9e38d1..5e5a3439 100644 --- a/src/home/CallList.test.tsx +++ b/src/home/CallList.test.tsx @@ -5,13 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { render, RenderResult } from "@testing-library/react"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { render, type RenderResult } from "@testing-library/react"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { MemoryRouter } from "react-router-dom"; import { describe, expect, it } from "vitest"; import { CallList } from "../../src/home/CallList"; -import { GroupCallRoom } from "../../src/home/useGroupCallRooms"; +import { type GroupCallRoom } from "../../src/home/useGroupCallRooms"; describe("CallList", () => { const renderComponent = (rooms: GroupCallRoom[]): RenderResult => { diff --git a/src/home/CallList.tsx b/src/home/CallList.tsx index 72b7356a..12bfae45 100644 --- a/src/home/CallList.tsx +++ b/src/home/CallList.tsx @@ -6,10 +6,10 @@ Please see LICENSE in the repository root for full details. */ import { Link } from "react-router-dom"; -import { MatrixClient } from "matrix-js-sdk/src/client"; -import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import { Room } from "matrix-js-sdk/src/models/room"; -import { FC, useCallback, MouseEvent, useState } from "react"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; +import { type RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { type Room } from "matrix-js-sdk/src/models/room"; +import { type FC, useCallback, type MouseEvent, useState } from "react"; import { useTranslation } from "react-i18next"; import { IconButton, Text } from "@vector-im/compound-web"; import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; @@ -18,7 +18,7 @@ import classNames from "classnames"; import { Avatar, Size } from "../Avatar"; import styles from "./CallList.module.css"; import { getRelativeRoomUrl } from "../utils/matrix"; -import { GroupCallRoom } from "./useGroupCallRooms"; +import { type GroupCallRoom } from "./useGroupCallRooms"; import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; interface CallListProps { diff --git a/src/home/HomePage.tsx b/src/home/HomePage.tsx index 74575494..9340ecc0 100644 --- a/src/home/HomePage.tsx +++ b/src/home/HomePage.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { useTranslation } from "react-i18next"; -import { FC } from "react"; +import { type FC } from "react"; import { useClientState } from "../ClientContext"; import { ErrorView, LoadingView } from "../FullScreenView"; diff --git a/src/home/JoinExistingCallModal.tsx b/src/home/JoinExistingCallModal.tsx index 6de3d2a9..3f2c3902 100644 --- a/src/home/JoinExistingCallModal.tsx +++ b/src/home/JoinExistingCallModal.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { useTranslation } from "react-i18next"; -import { FC, MouseEvent } from "react"; +import { type FC, type MouseEvent } from "react"; import { Button } from "@vector-im/compound-web"; import { Modal } from "../Modal"; diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index db242414..7a44ebb6 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -5,9 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { useState, useCallback, FormEvent, FormEventHandler, FC } from "react"; +import { + useState, + useCallback, + type FormEvent, + type FormEventHandler, + type FC, +} from "react"; import { useHistory } from "react-router-dom"; -import { MatrixClient } from "matrix-js-sdk/src/client"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; import { Heading, Text } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index daafa9f8..0e9be675 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback, useState, FormEventHandler } from "react"; +import { type FC, useCallback, useState, type FormEventHandler } from "react"; import { useHistory } from "react-router-dom"; import { randomString } from "matrix-js-sdk/src/randomstring"; import { Trans, useTranslation } from "react-i18next"; diff --git a/src/home/useGroupCallRooms.ts b/src/home/useGroupCallRooms.ts index 3946b51b..73464987 100644 --- a/src/home/useGroupCallRooms.ts +++ b/src/home/useGroupCallRooms.ts @@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { MatrixClient } from "matrix-js-sdk/src/client"; -import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; -import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; +import { type Room, RoomEvent } from "matrix-js-sdk/src/models/room"; +import { type RoomMember } from "matrix-js-sdk/src/models/room-member"; import { useState, useEffect } from "react"; import { EventTimeline, EventType, JoinRule } from "matrix-js-sdk/src/matrix"; -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager"; import { KnownMembership } from "matrix-js-sdk/src/types"; diff --git a/src/index.css b/src/index.css index bf6d1605..aeeccaf4 100644 --- a/src/index.css +++ b/src/index.css @@ -47,6 +47,11 @@ layer(compound); --background-gradient: url("graphics/backgroundGradient.svg"); } +:root, +[class*="cpd-theme-"] { + --video-tile-background: var(--cpd-color-bg-subtle-secondary); +} + .cpd-theme-dark { --cpd-color-border-accent: var(--cpd-color-green-1100); --stopgap-color-on-solid-accent: var(--cpd-color-text-primary); diff --git a/src/initializer.tsx b/src/initializer.tsx index 47634078..e9290504 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -24,7 +24,7 @@ import { platform } from "./Platform"; // This generates a map of locale names to their URL (based on import.meta.url), which looks like this: // { -// "../locales/en-GB/app.json": "/whatever/assets/root/locales/en-aabbcc.json", +// "../locales/en/app.json": "/whatever/assets/root/locales/en-aabbcc.json", // ... // } const locales = import.meta.glob("../locales/*/*.json", { @@ -41,7 +41,7 @@ const getLocaleUrl = ( const supportedLngs = [ ...new Set( Object.keys(locales).map((url) => { - // The URLs are of the form ../locales/en-GB/app.json + // The URLs are of the form ../locales/en/app.json // This extracts the language code from the URL const lang = url.match(/\/([^/]+)\/[^/]+\.json$/)?.[1]; if (!lang) { @@ -133,7 +133,7 @@ export class Initializer { .use(languageDetector) .use(initReactI18next) .init({ - fallbackLng: "en-GB", + fallbackLng: "en", defaultNS: "app", keySeparator: ".", nsSeparator: false, diff --git a/src/input/AvatarInputField.tsx b/src/input/AvatarInputField.tsx index 84eb48ac..a4bccb27 100644 --- a/src/input/AvatarInputField.tsx +++ b/src/input/AvatarInputField.tsx @@ -6,13 +6,13 @@ Please see LICENSE in the repository root for full details. */ import { - AllHTMLAttributes, + type AllHTMLAttributes, useEffect, useCallback, useState, - ChangeEvent, + type ChangeEvent, useRef, - FC, + type FC, } from "react"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; diff --git a/src/input/Input.tsx b/src/input/Input.tsx index cf2902cd..761988ad 100644 --- a/src/input/Input.tsx +++ b/src/input/Input.tsx @@ -6,11 +6,11 @@ Please see LICENSE in the repository root for full details. */ import { - ChangeEvent, - FC, - ForwardedRef, + type ChangeEvent, + type FC, + type ForwardedRef, forwardRef, - ReactNode, + type ReactNode, useId, } from "react"; import classNames from "classnames"; @@ -73,6 +73,7 @@ interface InputFieldProps { defaultValue?: string; placeholder?: string; defaultChecked?: boolean; + min?: number; onChange?: (event: ChangeEvent) => void; } @@ -91,6 +92,7 @@ export const InputField = forwardRef< suffix, description, disabled, + min, ...rest }, ref, @@ -127,6 +129,7 @@ export const InputField = forwardRef< checked={checked} disabled={disabled} aria-describedby={descriptionId} + min={min} {...rest} /> )} diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 5382b331..513e6b2f 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { - FC, + type FC, createContext, useCallback, useContext, @@ -25,7 +25,7 @@ import { audioInput as audioInputSetting, audioOutput as audioOutputSetting, videoInput as videoInputSetting, - Setting, + type Setting, } from "../settings/settings"; export type DeviceLabel = @@ -143,13 +143,13 @@ function useMediaDevice( ); } -const deviceStub: MediaDevice = { +export const deviceStub: MediaDevice = { available: new Map(), selectedId: undefined, selectedGroupId: undefined, select: () => {}, }; -const devicesStub: MediaDevices = { +export const devicesStub: MediaDevices = { audioInput: deviceStub, audioOutput: deviceStub, videoInput: deviceStub, @@ -157,7 +157,7 @@ const devicesStub: MediaDevices = { stopUsingDeviceNames: () => {}, }; -const MediaDevicesContext = createContext(devicesStub); +export const MediaDevicesContext = createContext(devicesStub); interface Props { children: JSX.Element; diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 3c77db2f..ab696d4e 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { IOpenIDToken, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type IOpenIDToken, type MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { useEffect, useState } from "react"; -import { LivekitFocus } from "matrix-js-sdk/src/matrixrtc/LivekitFocus"; +import { type LivekitFocus } from "matrix-js-sdk/src/matrixrtc/LivekitFocus"; import { useActiveLivekitFocus } from "../room/useActiveFocus"; diff --git a/src/livekit/options.ts b/src/livekit/options.ts index f7fe03ff..4f138585 100644 --- a/src/livekit/options.ts +++ b/src/livekit/options.ts @@ -8,10 +8,10 @@ Please see LICENSE in the repository root for full details. import { AudioPresets, DefaultReconnectPolicy, - RoomOptions, + type RoomOptions, ScreenSharePresets, - TrackPublishDefaults, - VideoPreset, + type TrackPublishDefaults, + type VideoPreset, VideoPresets, } from "livekit-client"; diff --git a/src/livekit/useECConnectionState.ts b/src/livekit/useECConnectionState.ts index 60c5b9bb..a99aa2e1 100644 --- a/src/livekit/useECConnectionState.ts +++ b/src/livekit/useECConnectionState.ts @@ -6,10 +6,10 @@ Please see LICENSE in the repository root for full details. */ import { - AudioCaptureOptions, + type AudioCaptureOptions, ConnectionState, - LocalTrack, - Room, + type LocalTrack, + type Room, RoomEvent, Track, } from "livekit-client"; @@ -17,7 +17,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import * as Sentry from "@sentry/react"; -import { SFUConfig, sfuConfigEquals } from "./openIDSFU"; +import { type SFUConfig, sfuConfigEquals } from "./openIDSFU"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; declare global { diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 458ecaa0..41b305e5 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -7,32 +7,32 @@ Please see LICENSE in the repository root for full details. import { ConnectionState, - E2EEOptions, + type E2EEManagerOptions, ExternalE2EEKeyProvider, Room, - RoomOptions, + type RoomOptions, Track, } from "livekit-client"; import { useEffect, useMemo, useRef } from "react"; import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { defaultLiveKitOptions } from "./options"; -import { SFUConfig } from "./openIDSFU"; -import { MuteStates } from "../room/MuteStates"; +import { type SFUConfig } from "./openIDSFU"; +import { type MuteStates } from "../room/MuteStates"; import { - MediaDevice, - MediaDevices, + type MediaDevice, + type MediaDevices, useMediaDevices, } from "./MediaDevicesContext"; import { - ECConnectionState, + type ECConnectionState, useECConnectionState, } from "./useECConnectionState"; import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider"; import { E2eeType } from "../e2ee/e2eeType"; -import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; interface UseLivekitResult { livekitRoom?: Room; @@ -45,7 +45,7 @@ export function useLiveKit( sfuConfig: SFUConfig | undefined, e2eeSystem: EncryptionSystem, ): UseLivekitResult { - const e2eeOptions = useMemo((): E2EEOptions | undefined => { + const e2eeOptions = useMemo((): E2EEManagerOptions | undefined => { if (e2eeSystem.kind === E2eeType.NONE) return undefined; if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) { diff --git a/src/otel/OTelCall.ts b/src/otel/OTelCall.ts index 586d410c..1bc349d3 100644 --- a/src/otel/OTelCall.ts +++ b/src/otel/OTelCall.ts @@ -5,17 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { Span } from "@opentelemetry/api"; -import { MatrixCall } from "matrix-js-sdk/src/matrix"; +import { type Span } from "@opentelemetry/api"; +import { type MatrixCall } from "matrix-js-sdk/src/matrix"; import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; import { - TransceiverStats, - CallFeedStats, + type TransceiverStats, + type CallFeedStats, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { ObjectFlattener } from "./ObjectFlattener"; import { ElementCallOpenTelemetry } from "./otel"; -import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; +import { type OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; import { OTelCallTransceiverMediaStreamSpan } from "./OTelCallTransceiverMediaStreamSpan"; import { OTelCallFeedMediaStreamSpan } from "./OTelCallFeedMediaStreamSpan"; diff --git a/src/otel/OTelCallAbstractMediaStreamSpan.ts b/src/otel/OTelCallAbstractMediaStreamSpan.ts index 98862597..59328250 100644 --- a/src/otel/OTelCallAbstractMediaStreamSpan.ts +++ b/src/otel/OTelCallAbstractMediaStreamSpan.ts @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import opentelemetry, { Span } from "@opentelemetry/api"; -import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; +import opentelemetry, { type Span } from "@opentelemetry/api"; +import { type TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; -import { ElementCallOpenTelemetry } from "./otel"; +import { type ElementCallOpenTelemetry } from "./otel"; import { OTelCallMediaStreamTrackSpan } from "./OTelCallMediaStreamTrackSpan"; type TrackId = string; diff --git a/src/otel/OTelCallFeedMediaStreamSpan.ts b/src/otel/OTelCallFeedMediaStreamSpan.ts index 68a683fb..5ba9a774 100644 --- a/src/otel/OTelCallFeedMediaStreamSpan.ts +++ b/src/otel/OTelCallFeedMediaStreamSpan.ts @@ -5,13 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { Span } from "@opentelemetry/api"; +import { type Span } from "@opentelemetry/api"; import { - CallFeedStats, - TrackStats, + type CallFeedStats, + type TrackStats, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; -import { ElementCallOpenTelemetry } from "./otel"; +import { type ElementCallOpenTelemetry } from "./otel"; import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { diff --git a/src/otel/OTelCallMediaStreamTrackSpan.ts b/src/otel/OTelCallMediaStreamTrackSpan.ts index cee2b298..50c4c028 100644 --- a/src/otel/OTelCallMediaStreamTrackSpan.ts +++ b/src/otel/OTelCallMediaStreamTrackSpan.ts @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; -import opentelemetry, { Span } from "@opentelemetry/api"; +import { type TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; +import opentelemetry, { type Span } from "@opentelemetry/api"; -import { ElementCallOpenTelemetry } from "./otel"; +import { type ElementCallOpenTelemetry } from "./otel"; export class OTelCallMediaStreamTrackSpan { private readonly span: Span; diff --git a/src/otel/OTelCallTransceiverMediaStreamSpan.ts b/src/otel/OTelCallTransceiverMediaStreamSpan.ts index c1fa33a2..a9f780ce 100644 --- a/src/otel/OTelCallTransceiverMediaStreamSpan.ts +++ b/src/otel/OTelCallTransceiverMediaStreamSpan.ts @@ -5,13 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { Span } from "@opentelemetry/api"; +import { type Span } from "@opentelemetry/api"; import { - TrackStats, - TransceiverStats, + type TrackStats, + type TransceiverStats, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; -import { ElementCallOpenTelemetry } from "./otel"; +import { type ElementCallOpenTelemetry } from "./otel"; import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index c9eded22..6854a6c4 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -5,31 +5,35 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import opentelemetry, { Span, Attributes, Context } from "@opentelemetry/api"; +import opentelemetry, { + type Span, + type Attributes, + type Context, +} from "@opentelemetry/api"; import { - GroupCall, - MatrixClient, - MatrixEvent, - RoomMember, + type GroupCall, + type MatrixClient, + type MatrixEvent, + type RoomMember, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { - CallError, - CallState, - MatrixCall, - VoipEvent, + type CallError, + type CallState, + type MatrixCall, + type VoipEvent, } from "matrix-js-sdk/src/webrtc/call"; import { - CallsByUserAndDevice, - GroupCallError, + type CallsByUserAndDevice, + type GroupCallError, GroupCallEvent, - GroupCallStatsReport, + type GroupCallStatsReport, } from "matrix-js-sdk/src/webrtc/groupCall"; import { - ConnectionStatsReport, - ByteSentStatsReport, - SummaryStatsReport, - CallFeedReport, + type ConnectionStatsReport, + type ByteSentStatsReport, + type SummaryStatsReport, + type CallFeedReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { ElementCallOpenTelemetry } from "./otel"; diff --git a/src/otel/ObjectFlattener.test.ts b/src/otel/ObjectFlattener.test.ts index 42f029cb..6a8de58b 100644 --- a/src/otel/ObjectFlattener.test.ts +++ b/src/otel/ObjectFlattener.test.ts @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; +import { type GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; import { - AudioConcealment, - ByteSentStatsReport, - ConnectionStatsReport, + type AudioConcealment, + type ByteSentStatsReport, + type ConnectionStatsReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { describe, expect, it } from "vitest"; diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts index ebf66975..622700f2 100644 --- a/src/otel/ObjectFlattener.ts +++ b/src/otel/ObjectFlattener.ts @@ -4,13 +4,13 @@ Copyright 2023, 2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { Attributes } from "@opentelemetry/api"; -import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; -import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; +import { type Attributes } from "@opentelemetry/api"; +import { type VoipEvent } from "matrix-js-sdk/src/webrtc/call"; +import { type GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; import { - ByteSentStatsReport, - ConnectionStatsReport, - SummaryStatsReport, + type ByteSentStatsReport, + type ConnectionStatsReport, + type SummaryStatsReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; export class ObjectFlattener { diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 14d31ec8..ec982975 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; -import opentelemetry, { Tracer } from "@opentelemetry/api"; +import opentelemetry, { type Tracer } from "@opentelemetry/api"; import { Resource } from "@opentelemetry/resources"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/profile/useProfile.ts b/src/profile/useProfile.ts index 6e04eec8..86164104 100644 --- a/src/profile/useProfile.ts +++ b/src/profile/useProfile.ts @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { MatrixClient } from "matrix-js-sdk/src/client"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { User, UserEvent } from "matrix-js-sdk/src/models/user"; -import { FileType } from "matrix-js-sdk/src/http-api"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; +import { type MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { type User, UserEvent } from "matrix-js-sdk/src/models/user"; +import { type FileType } from "matrix-js-sdk/src/http-api"; import { useState, useCallback, useEffect } from "react"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/reactions/RaisedHandIndicator.tsx b/src/reactions/RaisedHandIndicator.tsx index 8c4747b3..02ca068c 100644 --- a/src/reactions/RaisedHandIndicator.tsx +++ b/src/reactions/RaisedHandIndicator.tsx @@ -6,8 +6,8 @@ Please see LICENSE in the repository root for full details. */ import { - MouseEventHandler, - ReactNode, + type MouseEventHandler, + type ReactNode, useCallback, useEffect, useState, diff --git a/src/reactions/ReactionIndicator.module.css b/src/reactions/ReactionIndicator.module.css index cc05ab1a..0fba7351 100644 --- a/src/reactions/ReactionIndicator.module.css +++ b/src/reactions/ReactionIndicator.module.css @@ -1,6 +1,6 @@ .reactionIndicatorWidget { display: flex; - /* background-color: var(--cpd-color-bg-subtle-primary); */ + background-color: #00000030; border-radius: var(--cpd-radius-pill-effect); box-shadow: 0 0 var(--cpd-space-2x) #00000040; background: "ffffff40"; @@ -14,12 +14,15 @@ margin-top: auto; margin-bottom: auto; width: 3em; + padding-right: var(--cpd-space-2x); + margin-left: calc(var(--cpd-space-2x) * -1); } .reactionIndicatorWidgetLarge > p { padding: var(--cpd-space-2x); padding-right: var(--cpd-space-4x); padding-left: 0; + margin-left: 0; } .reactionLarge { @@ -30,14 +33,12 @@ .reaction { margin: var(--cpd-space-1x); - color: var(--cpd-color-icon-secondary); - /* background-color: var(--cpd-color-icon-secondary); */ + color: white; display: flex; align-items: center; border-radius: var(--cpd-radius-pill-effect); user-select: none; overflow: hidden; - /* box-shadow: var(--small-drop-shadow); */ box-sizing: border-box; max-inline-size: 100%; max-width: fit-content; diff --git a/src/reactions/ReactionIndicator.tsx b/src/reactions/ReactionIndicator.tsx index a664df18..e7066e11 100644 --- a/src/reactions/ReactionIndicator.tsx +++ b/src/reactions/ReactionIndicator.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { PropsWithChildren, ReactNode } from "react"; +import { type PropsWithChildren, type ReactNode } from "react"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; diff --git a/src/reactions/index.ts b/src/reactions/index.ts index 610e24f0..f8253c81 100644 --- a/src/reactions/index.ts +++ b/src/reactions/index.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { RelationType } from "matrix-js-sdk/src/types"; +import { type RelationType } from "matrix-js-sdk/src/types"; import catSoundOgg from "../sound/reactions/cat.ogg?url"; import catSoundMp3 from "../sound/reactions/cat.mp3?url"; diff --git a/src/room/AppSelectionModal.tsx b/src/room/AppSelectionModal.tsx index 588fceef..79df17c9 100644 --- a/src/room/AppSelectionModal.tsx +++ b/src/room/AppSelectionModal.tsx @@ -5,7 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, MouseEvent, useCallback, useMemo, useState } from "react"; +import { + type FC, + type MouseEvent, + useCallback, + useMemo, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import { Button, Text } from "@vector-im/compound-web"; import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; diff --git a/src/room/CallEndedView.tsx b/src/room/CallEndedView.tsx index 556dc6e5..3ff4f397 100644 --- a/src/room/CallEndedView.tsx +++ b/src/room/CallEndedView.tsx @@ -5,8 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, FormEventHandler, ReactNode, useCallback, useState } from "react"; -import { MatrixClient } from "matrix-js-sdk/src/client"; +import { + type FC, + type FormEventHandler, + type ReactNode, + useCallback, + useState, +} from "react"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; import { Trans, useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import { Button, Heading, Text } from "@vector-im/compound-web"; diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx new file mode 100644 index 00000000..e8d22704 --- /dev/null +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -0,0 +1,201 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { render } from "@testing-library/react"; +import { + afterAll, + beforeEach, + expect, + type MockedFunction, + test, + vitest, +} from "vitest"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; +import { ConnectionState } from "livekit-client"; +import { BehaviorSubject, of } from "rxjs"; +import { afterEach } from "node:test"; +import { act, type ReactNode } from "react"; +import { + type CallMembership, + type MatrixRTCSession, +} from "matrix-js-sdk/src/matrixrtc"; +import { type RoomMember } from "matrix-js-sdk/src/matrix"; + +import { + mockLivekitRoom, + mockLocalParticipant, + mockMatrixRoom, + mockMatrixRoomMember, + mockRemoteParticipant, + mockRtcMembership, + MockRTCSession, +} from "../utils/test"; +import { E2eeType } from "../e2ee/e2eeType"; +import { CallViewModel } from "../state/CallViewModel"; +import { + CallEventAudioRenderer, + MAX_PARTICIPANT_COUNT_FOR_SOUND, +} from "./CallEventAudioRenderer"; +import { useAudioContext } from "../useAudioContext"; +import { TestReactionsWrapper } from "../utils/testReactions"; +import { prefetchSounds } from "../soundUtils"; + +const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC"); +const local = mockMatrixRoomMember(localRtcMember); +const aliceRtcMember = mockRtcMembership("@alice:example.org", "AAAA"); +const alice = mockMatrixRoomMember(aliceRtcMember); +const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB"); +const localParticipant = mockLocalParticipant({ identity: "" }); +const aliceId = `${alice.userId}:${aliceRtcMember.deviceId}`; +const aliceParticipant = mockRemoteParticipant({ identity: aliceId }); + +vitest.mock("../useAudioContext"); +vitest.mock("../soundUtils"); + +afterEach(() => { + vitest.resetAllMocks(); +}); + +afterAll(() => { + vitest.restoreAllMocks(); +}); + +let playSound: MockedFunction< + NonNullable>["playSound"] +>; + +beforeEach(() => { + (prefetchSounds as MockedFunction).mockResolvedValue({ + sound: new ArrayBuffer(0), + }); + playSound = vitest.fn(); + (useAudioContext as MockedFunction).mockReturnValue({ + playSound, + }); +}); + +function TestComponent({ + rtcSession, + vm, +}: { + rtcSession: MockRTCSession; + vm: CallViewModel; +}): ReactNode { + return ( + + + + ); +} + +function getMockEnv( + members: RoomMember[], + initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember], +): { + vm: CallViewModel; + session: MockRTCSession; + remoteRtcMemberships: BehaviorSubject; +} { + const matrixRoomMembers = new Map(members.map((p) => [p.userId, p])); + const remoteParticipants = of([aliceParticipant]); + const liveKitRoom = mockLivekitRoom( + { localParticipant }, + { remoteParticipants }, + ); + const matrixRoom = mockMatrixRoom({ + client: { + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + on: vitest.fn(), + off: vitest.fn(), + } as Partial as MatrixClient, + getMember: (userId) => matrixRoomMembers.get(userId) ?? null, + }); + + const remoteRtcMemberships = new BehaviorSubject( + initialRemoteRtcMemberships, + ); + + const session = new MockRTCSession( + matrixRoom, + localRtcMember, + ).withMemberships(remoteRtcMemberships); + + const vm = new CallViewModel( + session as unknown as MatrixRTCSession, + liveKitRoom, + { + kind: E2eeType.PER_PARTICIPANT, + }, + of(ConnectionState.Connected), + ); + return { vm, session, remoteRtcMemberships }; +} + +/** + * We don't want to play a sound when loading the call state + * because typically this occurs in two stages. We first join + * the call as a local participant and *then* the remote + * participants join from our perspective. We don't want to make + * a noise every time. + */ +test("plays one sound when entering a call", () => { + const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]); + render(); + // Joining a call usually means remote participants are added later. + act(() => { + remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]); + }); + expect(playSound).toHaveBeenCalledOnce(); +}); + +// TODO: Same test? +test("plays a sound when a user joins", () => { + const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]); + render(); + + act(() => { + remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]); + }); + // Play a sound when joining a call. + expect(playSound).toBeCalledWith("join"); +}); + +test("plays a sound when a user leaves", () => { + const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]); + render(); + + act(() => { + remoteRtcMemberships.next([]); + }); + expect(playSound).toBeCalledWith("left"); +}); + +test("plays no sound when the participant list is more than the maximum size", () => { + const mockRtcMemberships: CallMembership[] = []; + for (let i = 0; i < MAX_PARTICIPANT_COUNT_FOR_SOUND; i++) { + mockRtcMemberships.push( + mockRtcMembership(`@user${i}:example.org`, `DEVICE${i}`), + ); + } + + const { session, vm, remoteRtcMemberships } = getMockEnv( + [local, alice], + mockRtcMemberships, + ); + + render(); + expect(playSound).not.toBeCalled(); + act(() => { + remoteRtcMemberships.next( + mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1), + ); + }); + expect(playSound).toBeCalledWith("left"); +}); diff --git a/src/room/CallEventAudioRenderer.tsx b/src/room/CallEventAudioRenderer.tsx new file mode 100644 index 00000000..a363c6f5 --- /dev/null +++ b/src/room/CallEventAudioRenderer.tsx @@ -0,0 +1,99 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { type ReactNode, useDeferredValue, useEffect, useMemo } from "react"; +import { filter, interval, throttle } from "rxjs"; + +import { type CallViewModel } from "../state/CallViewModel"; +import joinCallSoundMp3 from "../sound/join_call.mp3"; +import joinCallSoundOgg from "../sound/join_call.ogg"; +import leftCallSoundMp3 from "../sound/left_call.mp3"; +import leftCallSoundOgg from "../sound/left_call.ogg"; +import handSoundOgg from "../sound/raise_hand.ogg?url"; +import handSoundMp3 from "../sound/raise_hand.mp3?url"; +import { useAudioContext } from "../useAudioContext"; +import { prefetchSounds } from "../soundUtils"; +import { useReactions } from "../useReactions"; +import { useLatest } from "../useLatest"; + +// Do not play any sounds if the participant count has exceeded this +// number. +export const MAX_PARTICIPANT_COUNT_FOR_SOUND = 8; +export const THROTTLE_SOUND_EFFECT_MS = 500; + +export const callEventAudioSounds = prefetchSounds({ + join: { + mp3: joinCallSoundMp3, + ogg: joinCallSoundOgg, + }, + left: { + mp3: leftCallSoundMp3, + ogg: leftCallSoundOgg, + }, + raiseHand: { + mp3: handSoundMp3, + ogg: handSoundOgg, + }, +}); + +export function CallEventAudioRenderer({ + vm, +}: { + vm: CallViewModel; +}): ReactNode { + const audioEngineCtx = useAudioContext({ + sounds: callEventAudioSounds, + latencyHint: "interactive", + }); + const audioEngineRef = useLatest(audioEngineCtx); + + const { raisedHands } = useReactions(); + const raisedHandCount = useMemo( + () => Object.keys(raisedHands).length, + [raisedHands], + ); + const previousRaisedHandCount = useDeferredValue(raisedHandCount); + + useEffect(() => { + if (audioEngineRef.current && previousRaisedHandCount < raisedHandCount) { + void audioEngineRef.current.playSound("raiseHand"); + } + }, [audioEngineRef, previousRaisedHandCount, raisedHandCount]); + + useEffect(() => { + const joinSub = vm.memberChanges + .pipe( + filter( + ({ joined, ids }) => + ids.length <= MAX_PARTICIPANT_COUNT_FOR_SOUND && joined.length > 0, + ), + throttle(() => interval(THROTTLE_SOUND_EFFECT_MS)), + ) + .subscribe(() => { + void audioEngineRef.current?.playSound("join"); + }); + + const leftSub = vm.memberChanges + .pipe( + filter( + ({ ids, left }) => + ids.length <= MAX_PARTICIPANT_COUNT_FOR_SOUND && left.length > 0, + ), + throttle(() => interval(THROTTLE_SOUND_EFFECT_MS)), + ) + .subscribe(() => { + void audioEngineRef.current?.playSound("left"); + }); + + return (): void => { + joinSub.unsubscribe(); + leftSub.unsubscribe(); + }; + }, [audioEngineRef, vm]); + + return <>; +} diff --git a/src/room/EncryptionLock.tsx b/src/room/EncryptionLock.tsx index 74706be1..e93aec98 100644 --- a/src/room/EncryptionLock.tsx +++ b/src/room/EncryptionLock.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC } from "react"; +import { type FC } from "react"; import { Tooltip } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; import { diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx new file mode 100644 index 00000000..ea2cc5cf --- /dev/null +++ b/src/room/GroupCallView.test.tsx @@ -0,0 +1,153 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { beforeEach, expect, type MockedFunction, test, vitest } from "vitest"; +import { render } from "@testing-library/react"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; +import { of } from "rxjs"; +import { JoinRule, type RoomState } from "matrix-js-sdk/src/matrix"; +import { Router } from "react-router-dom"; +import { createBrowserHistory } from "history"; +import userEvent from "@testing-library/user-event"; + +import { type MuteStates } from "./MuteStates"; +import { prefetchSounds } from "../soundUtils"; +import { useAudioContext } from "../useAudioContext"; +import { ActiveCall } from "./InCallView"; +import { + mockMatrixRoom, + mockMatrixRoomMember, + mockRtcMembership, + MockRTCSession, +} from "../utils/test"; +import { GroupCallView } from "./GroupCallView"; +import { leaveRTCSession } from "../rtcSessionHelpers"; +import { type WidgetHelpers } from "../widget"; +import { LazyEventEmitter } from "../LazyEventEmitter"; + +vitest.mock("../soundUtils"); +vitest.mock("../useAudioContext"); +vitest.mock("./InCallView"); + +vitest.mock("../rtcSessionHelpers", async (importOriginal) => { + // TODO: perhaps there is a more elegant way to manage the type import here? + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const orig = await importOriginal(); + vitest.spyOn(orig, "leaveRTCSession"); + return orig; +}); + +let playSound: MockedFunction< + NonNullable>["playSound"] +>; + +const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC"); +const carol = mockMatrixRoomMember(localRtcMember); +const roomMembers = new Map([carol].map((p) => [p.userId, p])); + +const roomId = "!foo:bar"; +const soundPromise = Promise.resolve(true); + +beforeEach(() => { + (prefetchSounds as MockedFunction).mockResolvedValue({ + sound: new ArrayBuffer(0), + }); + playSound = vitest.fn().mockReturnValue(soundPromise); + (useAudioContext as MockedFunction).mockReturnValue({ + playSound, + }); + // A trivial implementation of Active call to ensure we are testing GroupCallView exclusively here. + (ActiveCall as MockedFunction).mockImplementation( + ({ onLeave }) => { + return ( +
+ +
+ ); + }, + ); +}); + +function createGroupCallView(widget: WidgetHelpers | null): { + rtcSession: MockRTCSession; + getByText: ReturnType["getByText"]; +} { + const history = createBrowserHistory(); + const client = { + getUser: () => null, + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + getRoom: (rId) => (rId === roomId ? room : null), + } as Partial as MatrixClient; + const room = mockMatrixRoom({ + client, + roomId, + getMember: (userId) => roomMembers.get(userId) ?? null, + getMxcAvatarUrl: () => null, + getCanonicalAlias: () => null, + currentState: { + getJoinRule: () => JoinRule.Invite, + } as Partial as RoomState, + }); + const rtcSession = new MockRTCSession( + room, + localRtcMember, + [], + ).withMemberships(of([])); + const muteState = { + audio: { enabled: false }, + video: { enabled: false }, + } as MuteStates; + const { getByText } = render( + + + , + ); + return { + getByText, + rtcSession, + }; +} + +test("will play a leave sound asynchronously in SPA mode", async () => { + const user = userEvent.setup(); + const { getByText, rtcSession } = createGroupCallView(null); + const leaveButton = getByText("Leave"); + await user.click(leaveButton); + expect(playSound).toHaveBeenCalledWith("left"); + expect(leaveRTCSession).toHaveBeenCalledWith(rtcSession, undefined); + expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce(); +}); + +test("will play a leave sound synchronously in widget mode", async () => { + const user = userEvent.setup(); + const widget = { + api: { + setAlwaysOnScreen: async () => Promise.resolve(true), + } as Partial, + lazyActions: new LazyEventEmitter(), + }; + const { getByText, rtcSession } = createGroupCallView( + widget as WidgetHelpers, + ); + const leaveButton = getByText("Leave"); + await user.click(leaveButton); + expect(playSound).toHaveBeenCalledWith("left"); + expect(leaveRTCSession).toHaveBeenCalledWith(rtcSession, soundPromise); + expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce(); +}); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index cc4fea07..3ea6a9c2 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -5,31 +5,45 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + type FC, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { useHistory } from "react-router-dom"; -import { MatrixClient } from "matrix-js-sdk/src/client"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; import { Room, isE2EESupported as isE2EESupportedBrowser, } from "livekit-client"; import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { JoinRule } from "matrix-js-sdk/src/matrix"; import { Heading, Text } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; import type { IWidgetApiRequest } from "matrix-widget-api"; -import { widget, ElementWidgetActions, JoinCallData } from "../widget"; +import { + ElementWidgetActions, + type JoinCallData, + type WidgetHelpers, +} from "../widget"; import { FullScreenView } from "../FullScreenView"; import { LobbyView } from "./LobbyView"; -import { MatrixInfo } from "./VideoPreview"; +import { type MatrixInfo } from "./VideoPreview"; import { CallEndedView } from "./CallEndedView"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useProfile } from "../profile/useProfile"; import { findDeviceByName } from "../utils/media"; import { ActiveCall } from "./InCallView"; -import { MUTE_PARTICIPANT_COUNT, MuteStates } from "./MuteStates"; -import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext"; +import { MUTE_PARTICIPANT_COUNT, type MuteStates } from "./MuteStates"; +import { + useMediaDevices, + type MediaDevices, +} from "../livekit/MediaDevicesContext"; import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships"; import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers"; import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState"; @@ -41,6 +55,9 @@ import { InviteModal } from "./InviteModal"; import { useUrlParams } from "../UrlParams"; import { E2eeType } from "../e2ee/e2eeType"; import { Link } from "../button/Link"; +import { useAudioContext } from "../useAudioContext"; +import { callEventAudioSounds } from "./CallEventAudioRenderer"; +import { useLatest } from "../useLatest"; declare global { interface Window { @@ -57,6 +74,7 @@ interface Props { hideHeader: boolean; rtcSession: MatrixRTCSession; muteStates: MuteStates; + widget: WidgetHelpers | null; } export const GroupCallView: FC = ({ @@ -68,10 +86,16 @@ export const GroupCallView: FC = ({ hideHeader, rtcSession, muteStates, + widget, }) => { const memberships = useMatrixRTCSessionMemberships(rtcSession); const isJoined = useMatrixRTCSessionJoinState(rtcSession); - + const leaveSoundContext = useLatest( + useAudioContext({ + sounds: callEventAudioSounds, + latencyHint: "interactive", + }), + ); // This should use `useEffectEvent` (only available in experimental versions) useEffect(() => { if (memberships.length >= MUTE_PARTICIPANT_COUNT) @@ -131,48 +155,46 @@ export const GroupCallView: FC = ({ const latestDevices = useRef(); latestDevices.current = deviceContext; + // TODO: why do we use a ref here instead of using muteStates directly? const latestMuteStates = useRef(); latestMuteStates.current = muteStates; useEffect(() => { - const defaultDeviceSetup = async ( - requestedDeviceData: JoinCallData, - ): Promise => { + const defaultDeviceSetup = async ({ + audioInput, + videoInput, + }: JoinCallData): Promise => { // XXX: I think this is broken currently - LiveKit *won't* request // 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 { audioInput, videoInput } = requestedDeviceData; - if (audioInput === null) { - latestMuteStates.current!.audio.setEnabled?.(false); - } else { + + if (audioInput) { const deviceId = findDeviceByName(audioInput, "audioinput", devices); if (!deviceId) { logger.warn("Unknown audio input: " + audioInput); + // override the default mute state latestMuteStates.current!.audio.setEnabled?.(false); } else { logger.debug( `Found audio input ID ${deviceId} for name ${audioInput}`, ); latestDevices.current!.audioInput.select(deviceId); - latestMuteStates.current!.audio.setEnabled?.(true); } } - if (videoInput === null) { - latestMuteStates.current!.video.setEnabled?.(false); - } else { + if (videoInput) { const deviceId = findDeviceByName(videoInput, "videoinput", devices); if (!deviceId) { logger.warn("Unknown video input: " + videoInput); + // override the default mute state latestMuteStates.current!.video.setEnabled?.(false); } else { logger.debug( `Found video input ID ${deviceId} for name ${videoInput}`, ); latestDevices.current!.videoInput.select(deviceId); - latestMuteStates.current!.video.setEnabled?.(true); } } }; @@ -187,19 +209,18 @@ export const GroupCallView: FC = ({ ev.detail.data as unknown as JoinCallData, ); await enterRTCSession(rtcSession, perParticipantE2EE); - widget!.api.transport.reply(ev.detail, {}); + widget.api.transport.reply(ev.detail, {}); })().catch((e) => { logger.error("Error joining RTC session", e); }); }; widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin); return (): void => { - widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin); + widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin); }; } else { // No lobby and no preload: we enter the rtc session right away (async (): Promise => { - await defaultDeviceSetup({ audioInput: null, videoInput: null }); await enterRTCSession(rtcSession, perParticipantE2EE); })().catch((e) => { logger.error("Error joining RTC session", e); @@ -209,7 +230,7 @@ export const GroupCallView: FC = ({ void enterRTCSession(rtcSession, perParticipantE2EE); } } - }, [rtcSession, preload, skipLobby, perParticipantE2EE]); + }, [widget, rtcSession, preload, skipLobby, perParticipantE2EE]); const [left, setLeft] = useState(false); const [leaveError, setLeaveError] = useState(undefined); @@ -217,12 +238,12 @@ export const GroupCallView: FC = ({ const onLeave = useCallback( (leaveError?: Error): void => { - setLeaveError(leaveError); - setLeft(true); - + const audioPromise = leaveSoundContext.current?.playSound("left"); // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent, // therefore we want the event to be sent instantly without getting queued/batched. const sendInstantly = !!widget; + setLeaveError(leaveError); + setLeft(true); PosthogAnalytics.instance.eventCallEnded.track( rtcSession.room.roomId, rtcSession.memberships.length, @@ -230,8 +251,12 @@ export const GroupCallView: FC = ({ rtcSession, ); - // Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts. - leaveRTCSession(rtcSession) + leaveRTCSession( + rtcSession, + // Wait for the sound in widget mode (it's not long) + sendInstantly && audioPromise ? audioPromise : undefined, + ) + // Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts. .then(() => { if ( !isPasswordlessUser && @@ -245,18 +270,25 @@ export const GroupCallView: FC = ({ logger.error("Error leaving RTC session", e); }); }, - [rtcSession, isPasswordlessUser, confineToRoom, history], + [ + widget, + rtcSession, + isPasswordlessUser, + confineToRoom, + leaveSoundContext, + history, + ], ); useEffect(() => { if (widget && isJoined) { // set widget to sticky once joined. - widget!.api.setAlwaysOnScreen(true).catch((e) => { + widget.api.setAlwaysOnScreen(true).catch((e) => { logger.error("Error calling setAlwaysOnScreen(true)", e); }); const onHangup = (ev: CustomEvent): void => { - widget!.api.transport.reply(ev.detail, {}); + widget.api.transport.reply(ev.detail, {}); // Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts. leaveRTCSession(rtcSession).catch((e) => { logger.error("Failed to leave RTC session", e); @@ -264,10 +296,10 @@ export const GroupCallView: FC = ({ }; widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup); return (): void => { - widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup); + widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup); }; } - }, [isJoined, rtcSession]); + }, [widget, isJoined, rtcSession]); const onReconnect = useCallback(() => { setLeft(false); @@ -334,7 +366,7 @@ export const GroupCallView: FC = ({ = ({ leaveError ) { return ( - + <> + + ; + ); } else { // If the user is a regular user, we'll have sent them back to the homepage, diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index fe973132..bf0aabf5 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -36,10 +36,9 @@ Please see LICENSE in the repository root for full details. inset-block-end: 0; z-index: 1; display: grid; - grid-template-columns: minmax(0, var(--inline-content-inset)) 1fr auto 1fr minmax( - 0, - var(--inline-content-inset) - ); + grid-template-columns: + minmax(0, var(--inline-content-inset)) + 1fr auto 1fr minmax(0, var(--inline-content-inset)); grid-template-areas: ". logo buttons layout ."; align-items: center; gap: var(--cpd-space-3x); diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index f4340f47..976e4e94 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -10,23 +10,22 @@ import { RoomContext, useLocalParticipant, } from "@livekit/components-react"; -import { ConnectionState, Room } from "livekit-client"; -import { MatrixClient } from "matrix-js-sdk/src/client"; +import { ConnectionState, type Room } from "livekit-client"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; import { - FC, - PointerEvent, - PropsWithoutRef, - TouchEvent, + type FC, + type PointerEvent, + type PropsWithoutRef, + type TouchEvent, forwardRef, useCallback, - useDeferredValue, useEffect, useMemo, useRef, useState, } from "react"; import useMeasure from "react-use-measure"; -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import classNames from "classnames"; import { BehaviorSubject, map } from "rxjs"; import { useObservable, useObservableEagerState } from "observable-hooks"; @@ -50,28 +49,32 @@ import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { ElementWidgetActions, widget } from "../widget"; import styles from "./InCallView.module.css"; import { GridTile } from "../tile/GridTile"; -import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; +import { type OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { useLiveKit } from "../livekit/useLiveKit"; import { useWakeLock } from "../useWakeLock"; import { useMergedRefs } from "../useMergedRefs"; -import { MuteStates } from "./MuteStates"; -import { MatrixInfo } from "./VideoPreview"; +import { type MuteStates } from "./MuteStates"; +import { type MatrixInfo } from "./VideoPreview"; import { InviteButton } from "../button/InviteButton"; import { LayoutToggle } from "./LayoutToggle"; -import { ECConnectionState } from "../livekit/useECConnectionState"; +import { type ECConnectionState } from "../livekit/useECConnectionState"; import { useOpenIDSFU } from "../livekit/openIDSFU"; -import { CallViewModel, GridMode, Layout } from "../state/CallViewModel"; -import { Grid, TileProps } from "../grid/Grid"; +import { + CallViewModel, + type GridMode, + type Layout, +} from "../state/CallViewModel"; +import { Grid, type TileProps } from "../grid/Grid"; import { useInitial } from "../useInitial"; import { SpotlightTile } from "../tile/SpotlightTile"; -import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; import { E2eeType } from "../e2ee/e2eeType"; import { makeGridLayout } from "../grid/GridLayout"; import { - CallLayoutOutputs, + type CallLayoutOutputs, defaultPipAlignment, defaultSpotlightAlignment, } from "../grid/CallLayout"; @@ -79,14 +82,16 @@ import { makeOneOnOneLayout } from "../grid/OneOnOneLayout"; import { makeSpotlightExpandedLayout } from "../grid/SpotlightExpandedLayout"; import { makeSpotlightLandscapeLayout } from "../grid/SpotlightLandscapeLayout"; import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout"; -import { GridTileViewModel, TileViewModel } from "../state/TileViewModel"; +import { GridTileViewModel, type TileViewModel } from "../state/TileViewModel"; import { ReactionsProvider, useReactions } from "../useReactions"; -import handSoundOgg from "../sound/raise_hand.ogg?url"; -import handSoundMp3 from "../sound/raise_hand.mp3?url"; import { ReactionsAudioRenderer } from "./ReactionAudioRenderer"; import { useSwitchCamera } from "./useSwitchCamera"; -import { soundEffectVolumeSetting, useSetting } from "../settings/settings"; import { ReactionsOverlay } from "./ReactionsOverlay"; +import { CallEventAudioRenderer } from "./CallEventAudioRenderer"; +import { + debugTileLayout as debugTileLayoutSetting, + useSetting, +} from "../settings/settings"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); @@ -123,7 +128,7 @@ export const ActiveCall: FC = (props) => { useEffect(() => { if (livekitRoom !== undefined) { const vm = new CallViewModel( - props.rtcSession.room, + props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable, @@ -131,12 +136,7 @@ export const ActiveCall: FC = (props) => { setVm(vm); return (): void => vm.destroy(); } - }, [ - props.rtcSession.room, - livekitRoom, - props.e2eeSystem, - connStateObservable, - ]); + }, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable]); if (livekitRoom === undefined || vm === null) return null; @@ -182,14 +182,7 @@ export const InCallView: FC = ({ connState, onShareClick, }) => { - const [soundEffectVolume] = useSetting(soundEffectVolumeSetting); - const { supportsReactions, raisedHands, sendReaction, toggleRaisedHand } = - useReactions(); - const raisedHandCount = useMemo( - () => Object.keys(raisedHands).length, - [raisedHands], - ); - const previousRaisedHandCount = useDeferredValue(raisedHandCount); + const { supportsReactions, sendReaction, toggleRaisedHand } = useReactions(); useWakeLock(); @@ -234,6 +227,8 @@ export const InCallView: FC = ({ const windowMode = useObservableEagerState(vm.windowMode); const layout = useObservableEagerState(vm.layout); + const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration); + const [debugTileLayout] = useSetting(debugTileLayoutSetting); const gridMode = useObservableEagerState(vm.gridMode); const showHeader = useObservableEagerState(vm.showHeader); const showFooter = useObservableEagerState(vm.showFooter); @@ -339,25 +334,6 @@ export const InCallView: FC = ({ [vm], ); - // Play a sound when the raised hand count increases. - const handRaisePlayer = useRef(null); - useEffect(() => { - if (!handRaisePlayer.current) { - return; - } - if (previousRaisedHandCount < raisedHandCount) { - handRaisePlayer.current.volume = soundEffectVolume; - handRaisePlayer.current.play().catch((ex) => { - logger.warn("Failed to play raise hand sound", ex); - }); - } - }, [ - raisedHandCount, - handRaisePlayer, - previousRaisedHandCount, - soundEffectVolume, - ]); - useEffect(() => { widget?.api.transport .send( @@ -615,6 +591,10 @@ export const InCallView: FC = ({ height={11} aria-label={import.meta.env.VITE_PRODUCT_NAME || "Element Call"} /> + {/* Don't mind this odd placement, it's just a little debug label */} + {debugTileLayout + ? `Tiles generation: ${tileStoreGeneration}` + : undefined} )} {showControls &&
{buttons}
} @@ -670,10 +650,7 @@ export const InCallView: FC = ({ ))} {renderContent()} - + {footer} diff --git a/src/room/InviteModal.test.tsx b/src/room/InviteModal.test.tsx index 45d903b0..ecd1ee48 100644 --- a/src/room/InviteModal.test.tsx +++ b/src/room/InviteModal.test.tsx @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { render, screen } from "@testing-library/react"; import { expect, test, vi } from "vitest"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { axe } from "vitest-axe"; import { BrowserRouter } from "react-router-dom"; import userEvent from "@testing-library/user-event"; diff --git a/src/room/InviteModal.tsx b/src/room/InviteModal.tsx index 4ef9a5a5..26bb6bc2 100644 --- a/src/room/InviteModal.tsx +++ b/src/room/InviteModal.tsx @@ -5,9 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, MouseEvent, useCallback, useMemo, useState } from "react"; +import { + type FC, + type MouseEvent, + useCallback, + useMemo, + useState, +} from "react"; import { useTranslation } from "react-i18next"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { Button, Text } from "@vector-im/compound-web"; import { LinkIcon, diff --git a/src/room/LayoutToggle.tsx b/src/room/LayoutToggle.tsx index 59dff95f..45cecb20 100644 --- a/src/room/LayoutToggle.tsx +++ b/src/room/LayoutToggle.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ChangeEvent, FC, TouchEvent, useCallback } from "react"; +import { type ChangeEvent, type FC, type TouchEvent, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Tooltip } from "@vector-im/compound-web"; import { diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index fd3df0c8..e7dfe3c5 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -5,15 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback, useMemo, useState } from "react"; +import { type FC, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { Button } from "@vector-im/compound-web"; import classNames from "classnames"; import { useHistory } from "react-router-dom"; import { logger } from "matrix-js-sdk/src/logger"; import { usePreviewTracks } from "@livekit/components-react"; -import { LocalVideoTrack, Track } from "livekit-client"; +import { type LocalVideoTrack, Track } from "livekit-client"; import { useObservable } from "observable-hooks"; import { map } from "rxjs"; @@ -21,8 +21,8 @@ import inCallStyles from "./InCallView.module.css"; import styles from "./LobbyView.module.css"; import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header"; import { useLocationNavigation } from "../useLocationNavigation"; -import { MatrixInfo, VideoPreview } from "./VideoPreview"; -import { MuteStates } from "./MuteStates"; +import { type MatrixInfo, VideoPreview } from "./VideoPreview"; +import { type MuteStates } from "./MuteStates"; import { InviteButton } from "../button/InviteButton"; import { EndCallButton, diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx new file mode 100644 index 00000000..719315e8 --- /dev/null +++ b/src/room/MuteStates.test.tsx @@ -0,0 +1,177 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; +import { type ReactNode } from "react"; +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; + +import { useMuteStates } from "./MuteStates"; +import { + type DeviceLabel, + type MediaDevice, + type MediaDevices, + MediaDevicesContext, +} from "../livekit/MediaDevicesContext"; +import { mockConfig } from "../utils/test"; + +function TestComponent(): ReactNode { + const muteStates = useMuteStates(); + return ( +
+
+ {muteStates.audio.enabled.toString()} +
+
+ {muteStates.video.enabled.toString()} +
+
+ ); +} + +const mockMicrophone: MediaDeviceInfo = { + deviceId: "", + kind: "audioinput", + label: "", + groupId: "", + toJSON() { + return {}; + }, +}; + +const mockSpeaker: MediaDeviceInfo = { + deviceId: "", + kind: "audiooutput", + label: "", + groupId: "", + toJSON() { + return {}; + }, +}; + +const mockCamera: MediaDeviceInfo = { + deviceId: "", + kind: "videoinput", + label: "", + groupId: "", + toJSON() { + return {}; + }, +}; + +function mockDevices(available: Map): MediaDevice { + return { + available, + selectedId: "", + selectedGroupId: "", + select: (): void => {}, + }; +} + +function mockMediaDevices( + { + microphone, + speaker, + camera, + }: { + microphone?: boolean; + speaker?: boolean; + camera?: boolean; + } = { microphone: true, speaker: true, camera: true }, +): MediaDevices { + return { + audioInput: mockDevices( + microphone + ? new Map([[mockMicrophone.deviceId, mockMicrophone]]) + : new Map(), + ), + audioOutput: mockDevices( + speaker ? new Map([[mockSpeaker.deviceId, mockSpeaker]]) : new Map(), + ), + videoInput: mockDevices( + camera ? new Map([[mockCamera.deviceId, mockCamera]]) : new Map(), + ), + startUsingDeviceNames: (): void => {}, + stopUsingDeviceNames: (): void => {}, + }; +} + +describe("useMuteStates", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + afterAll(() => { + vi.resetAllMocks(); + }); + + it("disabled when no input devices", () => { + mockConfig(); + + render( + + + + + , + ); + expect(screen.getByTestId("audio-enabled").textContent).toBe("false"); + expect(screen.getByTestId("video-enabled").textContent).toBe("false"); + }); + + it("should be enabled by default", () => { + mockConfig(); + + render( + + + + + , + ); + expect(screen.getByTestId("audio-enabled").textContent).toBe("true"); + expect(screen.getByTestId("video-enabled").textContent).toBe("true"); + }); + + it("uses defaults from config", () => { + mockConfig({ + media_devices: { + enable_audio: false, + enable_video: false, + }, + }); + + render( + + + + + , + ); + expect(screen.getByTestId("audio-enabled").textContent).toBe("false"); + expect(screen.getByTestId("video-enabled").textContent).toBe("false"); + }); + + it("skipLobby mutes inputs", () => { + mockConfig(); + + render( + + + + + , + ); + expect(screen.getByTestId("audio-enabled").textContent).toBe("false"); + expect(screen.getByTestId("video-enabled").textContent).toBe("false"); + }); +}); diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 5fcadc90..4a8aa9dd 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -6,19 +6,23 @@ Please see LICENSE in the repository root for full details. */ import { - Dispatch, - SetStateAction, + type Dispatch, + type SetStateAction, useCallback, useEffect, useMemo, } from "react"; -import { IWidgetApiRequest } from "matrix-widget-api"; +import { type IWidgetApiRequest } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; -import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext"; +import { + type MediaDevice, + useMediaDevices, +} from "../livekit/MediaDevicesContext"; import { useReactiveState } from "../useReactiveState"; import { ElementWidgetActions, widget } from "../widget"; import { Config } from "../config/Config"; +import { useUrlParams } from "../UrlParams"; /** * If there already are this many participants in the call, we automatically mute @@ -72,13 +76,14 @@ function useMuteState( export function useMuteStates(): MuteStates { const devices = useMediaDevices(); - const audio = useMuteState( - devices.audioInput, - () => Config.get().media_devices.enable_audio, - ); + const { skipLobby } = useUrlParams(); + + const audio = useMuteState(devices.audioInput, () => { + return Config.get().media_devices.enable_audio && !skipLobby; + }); const video = useMuteState( devices.videoInput, - () => Config.get().media_devices.enable_video, + () => Config.get().media_devices.enable_video && !skipLobby, ); useEffect(() => { diff --git a/src/room/RageshakeRequestModal.tsx b/src/room/RageshakeRequestModal.tsx index d22b0bea..d240cb73 100644 --- a/src/room/RageshakeRequestModal.tsx +++ b/src/room/RageshakeRequestModal.tsx @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useEffect } from "react"; +import { type FC, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Button, Text } from "@vector-im/compound-web"; -import { Modal, Props as ModalProps } from "../Modal"; +import { Modal, type Props as ModalProps } from "../Modal"; import { FieldRow, ErrorMessage } from "../input/Input"; import { useSubmitRageshake } from "../settings/submit-rageshake"; diff --git a/src/room/ReactionAudioRenderer.test.tsx b/src/room/ReactionAudioRenderer.test.tsx index cf9d7fad..0ab283a9 100644 --- a/src/room/ReactionAudioRenderer.test.tsx +++ b/src/room/ReactionAudioRenderer.test.tsx @@ -6,9 +6,18 @@ Please see LICENSE in the repository root for full details. */ import { render } from "@testing-library/react"; -import { afterAll, expect, test } from "vitest"; +import { + afterAll, + beforeEach, + expect, + test, + vitest, + type MockedFunction, + type Mock, +} from "vitest"; import { TooltipProvider } from "@vector-im/compound-web"; -import { act, ReactNode } from "react"; +import { act, type ReactNode } from "react"; +import { afterEach } from "node:test"; import { MockRoom, @@ -16,11 +25,13 @@ import { TestReactionsWrapper, } from "../utils/testReactions"; import { ReactionsAudioRenderer } from "./ReactionAudioRenderer"; -import { GenericReaction, ReactionSet } from "../reactions"; import { playReactionsSound, soundEffectVolumeSetting, } from "../settings/settings"; +import { useAudioContext } from "../useAudioContext"; +import { GenericReaction, ReactionSet } from "../reactions"; +import { prefetchSounds } from "../soundUtils"; const memberUserIdAlice = "@alice:example.org"; const memberUserIdBob = "@bob:example.org"; @@ -49,11 +60,31 @@ function TestComponent({ ); } -const originalPlayFn = window.HTMLMediaElement.prototype.play; -afterAll(() => { +vitest.mock("../useAudioContext"); +vitest.mock("../soundUtils"); + +afterEach(() => { + vitest.resetAllMocks(); playReactionsSound.setValue(playReactionsSound.defaultValue); soundEffectVolumeSetting.setValue(soundEffectVolumeSetting.defaultValue); - window.HTMLMediaElement.prototype.play = originalPlayFn; +}); + +afterAll(() => { + vitest.restoreAllMocks(); +}); + +let playSound: Mock< + NonNullable>["playSound"] +>; + +beforeEach(() => { + (prefetchSounds as MockedFunction).mockResolvedValue({ + sound: new ArrayBuffer(0), + }); + playSound = vitest.fn(); + (useAudioContext as MockedFunction).mockReturnValue({ + playSound, + }); }); test("preloads all audio elements", () => { @@ -62,29 +93,11 @@ test("preloads all audio elements", () => { new MockRoom(memberUserIdAlice), membership, ); - const { container } = render(); - expect(container.getElementsByTagName("audio")).toHaveLength( - // All reactions plus the generic sound - ReactionSet.filter((r) => r.sound).length + 1, - ); -}); - -test("loads no audio elements when disabled in settings", () => { - playReactionsSound.setValue(false); - const rtcSession = new MockRTCSession( - new MockRoom(memberUserIdAlice), - membership, - ); - const { container } = render(); - expect(container.getElementsByTagName("audio")).toHaveLength(0); + render(); + expect(prefetchSounds).toHaveBeenCalledOnce(); }); test("will play an audio sound when there is a reaction", () => { - const audioIsPlaying: string[] = []; - window.HTMLMediaElement.prototype.play = async function (): Promise { - audioIsPlaying.push((this.children[0] as HTMLSourceElement).src); - return Promise.resolve(); - }; playReactionsSound.setValue(true); const room = new MockRoom(memberUserIdAlice); const rtcSession = new MockRTCSession(room, membership); @@ -100,16 +113,10 @@ test("will play an audio sound when there is a reaction", () => { act(() => { room.testSendReaction(memberEventAlice, chosenReaction, membership); }); - expect(audioIsPlaying).toHaveLength(1); - expect(audioIsPlaying[0]).toContain(chosenReaction.sound?.ogg); + expect(playSound).toHaveBeenCalledWith(chosenReaction.name); }); test("will play the generic audio sound when there is soundless reaction", () => { - const audioIsPlaying: string[] = []; - window.HTMLMediaElement.prototype.play = async function (): Promise { - audioIsPlaying.push((this.children[0] as HTMLSourceElement).src); - return Promise.resolve(); - }; playReactionsSound.setValue(true); const room = new MockRoom(memberUserIdAlice); const rtcSession = new MockRTCSession(room, membership); @@ -125,38 +132,10 @@ test("will play the generic audio sound when there is soundless reaction", () => act(() => { room.testSendReaction(memberEventAlice, chosenReaction, membership); }); - expect(audioIsPlaying).toHaveLength(1); - expect(audioIsPlaying[0]).toContain(GenericReaction.sound?.ogg); -}); - -test("will play an audio sound with the correct volume", () => { - playReactionsSound.setValue(true); - soundEffectVolumeSetting.setValue(0.5); - const room = new MockRoom(memberUserIdAlice); - const rtcSession = new MockRTCSession(room, membership); - const { getByTestId } = render(); - - // Find the first reaction with a sound effect - const chosenReaction = ReactionSet.find((r) => !!r.sound); - if (!chosenReaction) { - throw Error( - "No reactions have sounds configured, this test cannot succeed", - ); - } - act(() => { - room.testSendReaction(memberEventAlice, chosenReaction, membership); - }); - expect((getByTestId(chosenReaction.name) as HTMLAudioElement).volume).toEqual( - 0.5, - ); + expect(playSound).toHaveBeenCalledWith(GenericReaction.name); }); test("will play multiple audio sounds when there are multiple different reactions", () => { - const audioIsPlaying: string[] = []; - window.HTMLMediaElement.prototype.play = async function (): Promise { - audioIsPlaying.push((this.children[0] as HTMLSourceElement).src); - return Promise.resolve(); - }; playReactionsSound.setValue(true); const room = new MockRoom(memberUserIdAlice); @@ -175,7 +154,6 @@ test("will play multiple audio sounds when there are multiple different reaction room.testSendReaction(memberEventBob, reaction2, membership); room.testSendReaction(memberEventCharlie, reaction1, membership); }); - expect(audioIsPlaying).toHaveLength(2); - expect(audioIsPlaying[0]).toContain(reaction1.sound?.ogg); - expect(audioIsPlaying[1]).toContain(reaction2.sound?.ogg); + expect(playSound).toHaveBeenCalledWith(reaction1.name); + expect(playSound).toHaveBeenCalledWith(reaction2.name); }); diff --git a/src/room/ReactionAudioRenderer.tsx b/src/room/ReactionAudioRenderer.tsx index cc0b4a57..be24a5d6 100644 --- a/src/room/ReactionAudioRenderer.tsx +++ b/src/room/ReactionAudioRenderer.tsx @@ -5,70 +5,67 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ReactNode, useEffect, useRef } from "react"; +import { type ReactNode, useDeferredValue, useEffect, useState } from "react"; import { useReactions } from "../useReactions"; -import { - playReactionsSound, - soundEffectVolumeSetting as effectSoundVolumeSetting, - useSetting, -} from "../settings/settings"; +import { playReactionsSound, useSetting } from "../settings/settings"; import { GenericReaction, ReactionSet } from "../reactions"; +import { useAudioContext } from "../useAudioContext"; +import { prefetchSounds } from "../soundUtils"; +import { useLatest } from "../useLatest"; + +const soundMap = Object.fromEntries([ + ...ReactionSet.filter((v) => v.sound !== undefined).map((v) => [ + v.name, + v.sound!, + ]), + [GenericReaction.name, GenericReaction.sound], +]); export function ReactionsAudioRenderer(): ReactNode { const { reactions } = useReactions(); const [shouldPlay] = useSetting(playReactionsSound); - const [effectSoundVolume] = useSetting(effectSoundVolumeSetting); - const audioElements = useRef>({}); + const [soundCache, setSoundCache] = useState | null>(null); + const audioEngineCtx = useAudioContext({ + sounds: soundCache, + latencyHint: "interactive", + }); + const audioEngineRef = useLatest(audioEngineCtx); + const oldReactions = useDeferredValue(reactions); useEffect(() => { - if (!audioElements.current) { + if (!shouldPlay || soundCache) { return; } + // This is fine even if we load the component multiple times, + // as the browser's cache should ensure once the media is loaded + // once that future fetches come via the cache. + setSoundCache(prefetchSounds(soundMap)); + }, [soundCache, shouldPlay]); - if (!shouldPlay) { + useEffect(() => { + if (!shouldPlay || !audioEngineRef.current) { return; } + const oldReactionSet = new Set( + Object.values(oldReactions).map((r) => r.name), + ); for (const reactionName of new Set( Object.values(reactions).map((r) => r.name), )) { - const audioElement = - audioElements.current[reactionName] ?? audioElements.current.generic; - if (audioElement?.paused) { - audioElement.volume = effectSoundVolume; - void audioElement.play(); + if (oldReactionSet.has(reactionName)) { + // Don't replay old reactions + return; + } + if (soundMap[reactionName]) { + void audioEngineRef.current.playSound(reactionName); + } else { + // Fallback sounds. + void audioEngineRef.current.playSound("generic"); } } - }, [audioElements, shouldPlay, reactions, effectSoundVolume]); - - // Do not render any audio elements if playback is disabled. Will save - // audio file fetches. - if (!shouldPlay) { - return null; - } - - // NOTE: We load all audio elements ahead of time to allow the cache - // to be populated, rather than risk a cache miss and have the audio - // be delayed. - return ( - <> - {[GenericReaction, ...ReactionSet].map( - (r) => - r.sound && ( - - ), - )} - - ); + }, [audioEngineRef, shouldPlay, oldReactions, reactions]); + return null; } diff --git a/src/room/ReactionsOverlay.test.tsx b/src/room/ReactionsOverlay.test.tsx index 121594ab..8ea17178 100644 --- a/src/room/ReactionsOverlay.test.tsx +++ b/src/room/ReactionsOverlay.test.tsx @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { render } from "@testing-library/react"; import { expect, test } from "vitest"; import { TooltipProvider } from "@vector-im/compound-web"; -import { act, ReactNode } from "react"; +import { act, type ReactNode } from "react"; import { afterEach } from "node:test"; import { diff --git a/src/room/ReactionsOverlay.tsx b/src/room/ReactionsOverlay.tsx index 7cdf7568..2f8daba5 100644 --- a/src/room/ReactionsOverlay.tsx +++ b/src/room/ReactionsOverlay.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ReactNode, useMemo } from "react"; +import { type ReactNode, useMemo } from "react"; import { useReactions } from "../useReactions"; import { diff --git a/src/room/RoomAuthView.tsx b/src/room/RoomAuthView.tsx index 2c7fd489..33aeb4c8 100644 --- a/src/room/RoomAuthView.tsx +++ b/src/room/RoomAuthView.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback, useState } from "react"; +import { type FC, useCallback, useState } from "react"; import { useLocation } from "react-router-dom"; import { Trans, useTranslation } from "react-i18next"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index 49d594bb..d8973c20 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useEffect, useState, ReactNode, useRef } from "react"; +import { type FC, useEffect, useState, type ReactNode, useRef } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { useTranslation } from "react-i18next"; import { CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; -import { MatrixError } from "matrix-js-sdk/src/http-api"; +import { type MatrixError } from "matrix-js-sdk/src/http-api"; import { Heading, Text } from "@vector-im/compound-web"; import { useClientLegacy } from "../ClientContext"; @@ -98,6 +98,7 @@ export const RoomPage: FC = () => { case "loaded": return ( { const focusFromOlderMembership = { @@ -34,8 +33,7 @@ test("It joins the correct Session", async () => { ], }; - vi.spyOn(Config, "get").mockReturnValue({ - ...DEFAULT_CONFIG, + mockConfig({ livekit: { livekit_service_url: "http://my-default-service-url.com" }, }); const mockedSession = vi.mocked({ diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index 9e699319..f1c7eb8c 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -5,18 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { logger } from "matrix-js-sdk/src/logger"; import { - LivekitFocus, - LivekitFocusActive, + type LivekitFocus, + type LivekitFocusActive, isLivekitFocus, isLivekitFocusConfig, } from "matrix-js-sdk/src/matrixrtc/LivekitFocus"; import { PosthogAnalytics } from "./analytics/PosthogAnalytics"; import { Config } from "./config/Config"; -import { ElementWidgetActions, WidgetHelpers, widget } from "./widget"; +import { ElementWidgetActions, type WidgetHelpers, widget } from "./widget"; const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci"; @@ -120,6 +120,7 @@ export async function enterRTCSession( const widgetPostHangupProcedure = async ( widget: WidgetHelpers, + promiseBeforeHangup?: Promise, ): Promise => { // we need to wait until the callEnded event is tracked on posthog. // Otherwise the iFrame gets killed before the callEnded event got tracked. @@ -132,6 +133,8 @@ const widgetPostHangupProcedure = async ( logger.error("Failed to set call widget `alwaysOnScreen` to false", e); } + // Wait for any last bits before hanging up. + await promiseBeforeHangup; // We send the hangup event after the memberships have been updated // calling leaveRTCSession. // We need to wait because this makes the client hosting this widget killing the IFrame. @@ -140,9 +143,12 @@ const widgetPostHangupProcedure = async ( export async function leaveRTCSession( rtcSession: MatrixRTCSession, + promiseBeforeHangup?: Promise, ): Promise { await rtcSession.leaveRoomSession(); if (widget) { - await widgetPostHangupProcedure(widget); + await widgetPostHangupProcedure(widget, promiseBeforeHangup); + } else { + await promiseBeforeHangup; } } diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx new file mode 100644 index 00000000..057b0b0c --- /dev/null +++ b/src/settings/DeveloperSettingsTab.tsx @@ -0,0 +1,108 @@ +/* +Copyright 2022-2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { type ChangeEvent, type FC, useCallback } from "react"; +import { useTranslation } from "react-i18next"; + +import { FieldRow, InputField } from "../input/Input"; +import { + useSetting, + duplicateTiles as duplicateTilesSetting, + debugTileLayout as debugTileLayoutSetting, + showNonMemberTiles as showNonMemberTilesSetting, +} from "./settings"; +import type { MatrixClient } from "matrix-js-sdk/src/client"; + +interface Props { + client: MatrixClient; +} + +export const DeveloperSettingsTab: FC = ({ client }) => { + const { t } = useTranslation(); + const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting); + const [debugTileLayout, setDebugTileLayout] = useSetting( + debugTileLayoutSetting, + ); + const [showNonMemberTiles, setShowNonMemberTiles] = useSetting( + showNonMemberTilesSetting, + ); + + return ( + <> +

+ {t("developer_mode.hostname", { + hostname: window.location.hostname || "unknown", + })} +

+

+ {t("version", { + productName: import.meta.env.VITE_PRODUCT_NAME || "Element Call", + version: import.meta.env.VITE_APP_VERSION || "dev", + })} +

+

+ {t("developer_mode.crypto_version", { + version: client.getCrypto()?.getVersion() || "unknown", + })} +

+

+ {t("developer_mode.matrix_id", { + id: client.getUserId() || "unknown", + })} +

+

+ {t("developer_mode.device_id", { + id: client.getDeviceId() || "unknown", + })} +

+ + ): void => { + const value = event.target.valueAsNumber; + if (value < 0) { + return; + } + setDuplicateTiles(Number.isNaN(value) ? 0 : value); + }, + [setDuplicateTiles], + )} + /> + + + ): void => + setDebugTileLayout(event.target.checked) + } + /> + + + ): void => { + setShowNonMemberTiles(event.target.checked); + }, + [setShowNonMemberTiles], + )} + /> + + + ); +}; diff --git a/src/settings/DeviceSelection.tsx b/src/settings/DeviceSelection.tsx index c4020822..9faa1e82 100644 --- a/src/settings/DeviceSelection.tsx +++ b/src/settings/DeviceSelection.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ChangeEvent, FC, useCallback, useId } from "react"; +import { type ChangeEvent, type FC, useCallback, useId } from "react"; import { Heading, InlineField, @@ -15,7 +15,7 @@ import { } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; -import { MediaDevice } from "../livekit/MediaDevicesContext"; +import { type MediaDevice } from "../livekit/MediaDevicesContext"; import styles from "./DeviceSelection.module.css"; interface Props { diff --git a/src/settings/FeedbackSettingsTab.tsx b/src/settings/FeedbackSettingsTab.tsx index 455995a1..78a116cd 100644 --- a/src/settings/FeedbackSettingsTab.tsx +++ b/src/settings/FeedbackSettingsTab.tsx @@ -5,15 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback } from "react"; +import { type ChangeEvent, type FC, useCallback } from "react"; import { randomString } from "matrix-js-sdk/src/randomstring"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { Button, Text } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/src/logger"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake"; import feedbackStyles from "../input/FeedbackInput.module.css"; +import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; +import { useOptInAnalytics } from "./settings"; interface Props { roomId?: string; @@ -52,8 +54,32 @@ export const FeedbackSettingsTab: FC = ({ roomId }) => { [submitRageshake, roomId, sendRageshakeRequest], ); + const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); + const optInDescription = ( + + + +
+ You may withdraw consent by unchecking this box. If you are currently in + a call, this setting will take effect at the end of the call. +
+
+ ); + return (
+

{t("common.analytics")}

+ + ): void => { + setOptInAnalytics?.(event.target.checked); + }} + /> +

{t("settings.feedback_tab_h4")}

{t("settings.feedback_tab_body")}
diff --git a/src/settings/PreferencesSettingsTab.tsx b/src/settings/PreferencesSettingsTab.tsx index bc98d181..72d2d919 100644 --- a/src/settings/PreferencesSettingsTab.tsx +++ b/src/settings/PreferencesSettingsTab.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ChangeEvent, FC } from "react"; +import { type ChangeEvent, type FC } from "react"; import { useTranslation } from "react-i18next"; import { Text } from "@vector-im/compound-web"; @@ -14,6 +14,7 @@ import { showHandRaisedTimer as showHandRaisedTimerSetting, showReactions as showReactionsSetting, playReactionsSound as playReactionsSoundSetting, + developerMode as developerModeSetting, useSetting, } from "./settings"; @@ -36,23 +37,23 @@ export const PreferencesSettingsTab: FC = () => { fn(e.target.checked); }; + const [developerMode, setDeveloperMode] = useSetting(developerModeSetting); + return (
-

{t("settings.preferences_tab_h4")}

- {t("settings.preferences_tab_body")} + {t("settings.preferences_tab.introduction")} onChangeSetting(e, setShowHandRaisedTimer)} /> -
{t("settings.preferences_tab.reactions_title")}
{ onChange={(e) => onChangeSetting(e, setPlayReactionSound)} /> + + ): void => + setDeveloperMode(event.target.checked) + } + /> +
); }; diff --git a/src/settings/ProfileSettingsTab.tsx b/src/settings/ProfileSettingsTab.tsx index 4eb5b0d9..94d43f04 100644 --- a/src/settings/ProfileSettingsTab.tsx +++ b/src/settings/ProfileSettingsTab.tsx @@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback, useEffect, useMemo, useRef } from "react"; -import { MatrixClient } from "matrix-js-sdk/src/client"; +import { type FC, useCallback, useEffect, useMemo, useRef } from "react"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/settings/RageshakeButton.tsx b/src/settings/RageshakeButton.tsx index 0854da6e..fa17b788 100644 --- a/src/settings/RageshakeButton.tsx +++ b/src/settings/RageshakeButton.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { useTranslation } from "react-i18next"; -import { FC, useCallback } from "react"; +import { type FC, useCallback } from "react"; import { Button } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 3ac980ca..b7066095 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -5,16 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ChangeEvent, FC, useCallback } from "react"; -import { Trans, useTranslation } from "react-i18next"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { Root as Form, Text } from "@vector-im/compound-web"; +import { type FC, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { Root as Form } from "@vector-im/compound-web"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; -import { Tab, TabContainer } from "../tabs/Tabs"; -import { FieldRow, InputField } from "../input/Input"; -import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; +import { type Tab, TabContainer } from "../tabs/Tabs"; import { ProfileSettingsTab } from "./ProfileSettingsTab"; import { FeedbackSettingsTab } from "./FeedbackSettingsTab"; import { @@ -24,14 +22,13 @@ import { import { widget } from "../widget"; import { useSetting, - developerSettingsTab as developerSettingsTabSetting, - duplicateTiles as duplicateTilesSetting, - useOptInAnalytics, soundEffectVolumeSetting, + developerMode, } from "./settings"; import { PreferencesSettingsTab } from "./PreferencesSettingsTab"; import { Slider } from "../Slider"; import { DeviceSelection } from "./DeviceSelection"; +import { DeveloperSettingsTab } from "./DeveloperSettingsTab"; type SettingsTab = | "audio" @@ -63,27 +60,12 @@ export const SettingsModal: FC = ({ }) => { const { t } = useTranslation(); - const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); - const [developerSettingsTab, setDeveloperSettingsTab] = useSetting( - developerSettingsTabSetting, - ); - const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting); - - const optInDescription = ( - - - -
- You may withdraw consent by unchecking this box. If you are currently in - a call, this setting will take effect at the end of the call. -
-
- ); - const devices = useMediaDevices(); useMediaDeviceNames(devices, open); - const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting); + const [soundVolumeRaw, setSoundVolumeRaw] = useState(soundVolume); + + const [showDeveloperSettingsTab] = useSetting(developerMode); const audioTab: Tab = { key: "audio", @@ -108,8 +90,9 @@ export const SettingsModal: FC = ({

{t("settings.audio_tab.effect_volume_description")}

= ({ content: , }; - const moreTab: Tab = { - key: "more", - name: t("settings.more_tab_title"), - content: ( - <> -

{t("settings.developer_tab_title")}

-

- {t("version", { - productName: import.meta.env.VITE_PRODUCT_NAME || "Element Call", - version: import.meta.env.VITE_APP_VERSION || "dev", - })} -

- - ): void => - setDeveloperSettingsTab(event.target.checked) - } - /> - -

{t("common.analytics")}

- - ): void => { - setOptInAnalytics?.(event.target.checked); - }} - /> - - - ), - }; - const developerTab: Tab = { key: "developer", name: t("settings.developer_tab_title"), - content: ( - <> -

- {t("version", { - productName: import.meta.env.VITE_PRODUCT_NAME || "Element Call", - version: import.meta.env.VITE_APP_VERSION || "dev", - })} -

-

- {t("crypto_version", { - version: client.getCrypto()?.getVersion() || "unknown", - })} -

-

- {t("matrix_id", { - id: client.getUserId() || "unknown", - })} -

-

- {t("device_id", { - id: client.getDeviceId() || "unknown", - })} -

- - ): void => { - const value = event.target.valueAsNumber; - setDuplicateTiles(Number.isNaN(value) ? 0 : value); - }, - [setDuplicateTiles], - )} - /> - - - ), + content: , }; const tabs = [audioTab, videoTab]; if (widget === null) tabs.push(profileTab); - tabs.push(preferencesTab, feedbackTab, moreTab); - if (developerSettingsTab) tabs.push(developerTab); + tabs.push(preferencesTab, feedbackTab); + if (showDeveloperSettingsTab) tabs.push(developerTab); return ( ( + "show-non-member-tiles", + false, +); +export const debugTileLayout = new Setting("debug-tile-layout", false); + export const audioInput = new Setting( "audio-input", undefined, diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index ae320493..9a3529d5 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -5,20 +5,20 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ComponentProps, useCallback, useEffect, useState } from "react"; +import { type ComponentProps, useCallback, useEffect, useState } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { ClientEvent, - Crypto, - MatrixClient, - MatrixEvent, + type Crypto, + type MatrixClient, + type MatrixEvent, } from "matrix-js-sdk/src/matrix"; import { getLogsForReport } from "./rageshake"; import { useClient } from "../ClientContext"; import { Config } from "../config/Config"; import { ElementCallOpenTelemetry } from "../otel/otel"; -import { RageshakeRequestModal } from "../room/RageshakeRequestModal"; +import { type RageshakeRequestModal } from "../room/RageshakeRequestModal"; const gzip = async (text: string): Promise => { // pako is relatively large (200KB), so we only import it when needed diff --git a/src/sound/LICENCE.md b/src/sound/LICENCE.md index 769d05a4..a984803a 100644 --- a/src/sound/LICENCE.md +++ b/src/sound/LICENCE.md @@ -19,4 +19,6 @@ The following sound effects have been originally created by Element. - `end_talk` - `start_talk_local` - `start_talk_remote` +- `join_call` +- `end_call` - `reactions/rock` diff --git a/src/sound/join_call.mp3 b/src/sound/join_call.mp3 new file mode 100644 index 00000000..523d0d18 Binary files /dev/null and b/src/sound/join_call.mp3 differ diff --git a/src/sound/join_call.ogg b/src/sound/join_call.ogg new file mode 100644 index 00000000..27ab3b9d Binary files /dev/null and b/src/sound/join_call.ogg differ diff --git a/src/sound/left_call.mp3 b/src/sound/left_call.mp3 new file mode 100644 index 00000000..2eaac386 Binary files /dev/null and b/src/sound/left_call.mp3 differ diff --git a/src/sound/left_call.ogg b/src/sound/left_call.ogg new file mode 100644 index 00000000..ee9d582b Binary files /dev/null and b/src/sound/left_call.ogg differ diff --git a/src/soundUtils.ts b/src/soundUtils.ts new file mode 100644 index 00000000..162091b7 --- /dev/null +++ b/src/soundUtils.ts @@ -0,0 +1,63 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; + +type SoundDefinition = { mp3?: string; ogg: string }; + +export type PrefetchedSounds = Promise< + Record +>; + +/** + * Determine the best format we can use to play our sounds + * through. We prefer ogg support if possible, but will fall + * back to MP3. + * @returns "ogg" if the browser is likely to support it, or "mp3" otherwise. + */ +function getPreferredAudioFormat(): "ogg" | "mp3" { + const a = document.createElement("audio"); + if (a.canPlayType("audio/ogg") === "maybe") { + return "ogg"; + } + // Otherwise just assume MP3, as that has a chance of being more widely supported. + return "mp3"; +} + +const preferredFormat = getPreferredAudioFormat(); + +/** + * Prefetch sounds to be used by the AudioContext. This can + * be called outside the scope of a component to ensure the + * sounds load ahead of time. + * @param sounds A set of sound files that may be played. + * @returns A map of sound files to buffers. + */ +export async function prefetchSounds( + sounds: Record, +): PrefetchedSounds { + const buffers: Record = {}; + await Promise.all( + Object.entries(sounds).map(async ([name, file]) => { + const { mp3, ogg } = file as SoundDefinition; + // Use preferred format, fallback to ogg if no mp3 is provided. + // Load an audio file + const response = await fetch( + preferredFormat === "ogg" ? ogg : (mp3 ?? ogg), + ); + if (!response.ok) { + // 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`); + return; + } + // Decode it + buffers[name] = await response.arrayBuffer(); + }), + ); + return buffers as Record; +} diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel.test.ts index aa49f048..d5b84d49 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel.test.ts @@ -5,27 +5,32 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { test, vi, onTestFinished } from "vitest"; +import { test, vi, onTestFinished, it } from "vitest"; import { combineLatest, debounceTime, distinctUntilChanged, map, - Observable, + type Observable, of, switchMap, } from "rxjs"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { ConnectionState, - LocalParticipant, - Participant, - RemoteParticipant, + type LocalParticipant, + type Participant, + ParticipantEvent, + type RemoteParticipant, } from "livekit-client"; import * as ComponentsCore from "@livekit/components-core"; import { isEqual } from "lodash-es"; +import { + type CallMembership, + type MatrixRTCSession, +} from "matrix-js-sdk/src/matrixrtc"; -import { CallViewModel, Layout } from "./CallViewModel"; +import { CallViewModel, type Layout } from "./CallViewModel"; import { mockLivekitRoom, mockLocalParticipant, @@ -33,23 +38,31 @@ import { mockMatrixRoomMember, mockRemoteParticipant, withTestScheduler, + mockRtcMembership, + MockRTCSession, } from "../utils/test"; import { ECAddonConnectionState, - ECConnectionState, + type ECConnectionState, } from "../livekit/useECConnectionState"; import { E2eeType } from "../e2ee/e2eeType"; +import { showNonMemberTiles } from "../settings/settings"; vi.mock("@livekit/components-core"); -const alice = mockMatrixRoomMember({ userId: "@alice:example.org" }); -const bob = mockMatrixRoomMember({ userId: "@bob:example.org" }); -const carol = mockMatrixRoomMember({ userId: "@carol:example.org" }); -const dave = mockMatrixRoomMember({ userId: "@dave:example.org" }); +const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC"); +const aliceRtcMember = mockRtcMembership("@alice:example.org", "AAAA"); +const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB"); +const daveRtcMember = mockRtcMembership("@dave:example.org", "DDDD"); -const aliceId = `${alice.userId}:AAAA`; -const bobId = `${bob.userId}:BBBB`; -const daveId = `${dave.userId}:DDDD`; +const alice = mockMatrixRoomMember(aliceRtcMember); +const bob = mockMatrixRoomMember(bobRtcMember); +const carol = mockMatrixRoomMember(localRtcMember); +const dave = mockMatrixRoomMember(daveRtcMember); + +const aliceId = `${alice.userId}:${aliceRtcMember.deviceId}`; +const bobId = `${bob.userId}:${bobRtcMember.deviceId}`; +const daveId = `${dave.userId}:${daveRtcMember.deviceId}`; const localParticipant = mockLocalParticipant({ identity: "" }); const aliceParticipant = mockRemoteParticipant({ identity: aliceId }); @@ -64,7 +77,9 @@ const bobSharingScreen = mockRemoteParticipant({ }); const daveParticipant = mockRemoteParticipant({ identity: daveId }); -const members = new Map([alice, bob, carol, dave].map((p) => [p.userId, p])); +const roomMembers = new Map( + [alice, bob, carol, dave].map((p) => [p.userId, p]), +); export interface GridLayoutSummary { type: "grid"; @@ -172,10 +187,23 @@ function summarizeLayout(l: Observable): Observable { function withCallViewModel( remoteParticipants: Observable, + rtcMembers: Observable[]>, connectionState: Observable, speaking: Map>, continuation: (vm: CallViewModel) => void, ): void { + const room = mockMatrixRoom({ + client: { + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + } as Partial as MatrixClient, + getMember: (userId) => roomMembers.get(userId) ?? null, + }); + const rtcSession = new MockRTCSession( + room, + localRtcMember, + [], + ).withMemberships(rtcMembers); const participantsSpy = vi .spyOn(ComponentsCore, "connectedParticipantsObserver") .mockReturnValue(remoteParticipants); @@ -188,11 +216,15 @@ function withCallViewModel( ); const eventsSpy = vi .spyOn(ComponentsCore, "observeParticipantEvents") - .mockImplementation((p) => - (speaking.get(p) ?? of(false)).pipe( - map((s) => ({ ...p, isSpeaking: s }) as Participant), - ), - ); + .mockImplementation((p, ...eventTypes) => { + if (eventTypes.includes(ParticipantEvent.IsSpeakingChanged)) { + return (speaking.get(p) ?? of(false)).pipe( + map((s) => ({ ...p, isSpeaking: s }) as Participant), + ); + } else { + return of(p); + } + }); const roomEventSelectorSpy = vi .spyOn(ComponentsCore, "roomEventSelector") @@ -204,12 +236,7 @@ function withCallViewModel( ); const vm = new CallViewModel( - mockMatrixRoom({ - client: { - getUserId: () => "@carol:example.org", - } as Partial as MatrixClient, - getMember: (userId) => members.get(userId) ?? null, - }), + rtcSession as unknown as MatrixRTCSession, liveKitRoom, { kind: E2eeType.PER_PARTICIPANT, @@ -242,6 +269,7 @@ test("participants are retained during a focus switch", () => { a: [aliceParticipant, bobParticipant], b: [], }), + of([aliceRtcMember, bobRtcMember]), hot(connectionInputMarbles, { c: ConnectionState.Connected, s: ECAddonConnectionState.ECSwitchingFocus, @@ -283,6 +311,7 @@ test("screen sharing activates spotlight layout", () => { c: [aliceSharingScreen, bobSharingScreen], d: [aliceParticipant, bobSharingScreen], }), + of([aliceRtcMember, bobRtcMember]), of(ConnectionState.Connected), new Map(), (vm) => { @@ -348,19 +377,20 @@ test("screen sharing activates spotlight layout", () => { test("participants stay in the same order unless to appear/disappear", () => { withTestScheduler(({ hot, schedule, expectObservable }) => { - const modeInputMarbles = " a"; + const visibilityInputMarbles = "a"; // First Bob speaks, then Dave, then Alice - const aSpeakingInputMarbles = "n- 1998ms - 1999ms y"; - const bSpeakingInputMarbles = "ny 1998ms n 1999ms "; - const dSpeakingInputMarbles = "n- 1998ms y 1999ms n"; + const aSpeakingInputMarbles = " n- 1998ms - 1999ms y"; + const bSpeakingInputMarbles = " ny 1998ms n 1999ms -"; + const dSpeakingInputMarbles = " n- 1998ms y 1999ms n"; // Nothing should change when Bob speaks, because Bob is already on screen. // When Dave speaks he should switch with Alice because she's the one who // hasn't spoken at all. Then when Alice speaks, she should return to her // place at the top. - const expectedLayoutMarbles = "a 1999ms b 1999ms a 57999ms c 1999ms a"; + const expectedLayoutMarbles = " a 1999ms b 1999ms a 57999ms c 1999ms a"; withCallViewModel( of([aliceParticipant, bobParticipant, daveParticipant]), + of([aliceRtcMember, bobRtcMember, daveRtcMember]), of(ConnectionState.Connected), new Map([ [aliceParticipant, hot(aSpeakingInputMarbles, { y: true, n: false })], @@ -368,15 +398,12 @@ test("participants stay in the same order unless to appear/disappear", () => { [daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })], ]), (vm) => { - schedule(modeInputMarbles, { + schedule(visibilityInputMarbles, { a: () => { // We imagine that only three tiles (the first three) will be visible // on screen at a time vm.layout.subscribe((layout) => { - if (layout.type === "grid") { - for (let i = 0; i < layout.grid.length; i++) - layout.grid[i].setVisible(i < 3); - } + if (layout.type === "grid") layout.setVisibleTiles(3); }); }, }); @@ -406,8 +433,58 @@ test("participants stay in the same order unless to appear/disappear", () => { }); }); +test("participants adjust order when space becomes constrained", () => { + withTestScheduler(({ hot, schedule, expectObservable }) => { + // Start with all tiles on screen then shrink to 3 + const visibilityInputMarbles = "a-b"; + // Bob and Dave speak + const bSpeakingInputMarbles = " ny"; + const dSpeakingInputMarbles = " ny"; + // Nothing should change when Bob or Dave initially speak, because they are + // on screen. When the screen becomes smaller Alice should move off screen + // to make way for the speakers (specifically, she should swap with Dave). + const expectedLayoutMarbles = " a-b"; + + withCallViewModel( + of([aliceParticipant, bobParticipant, daveParticipant]), + of([aliceRtcMember, bobRtcMember, daveRtcMember]), + of(ConnectionState.Connected), + new Map([ + [bobParticipant, hot(bSpeakingInputMarbles, { y: true, n: false })], + [daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })], + ]), + (vm) => { + let setVisibleTiles: ((value: number) => void) | null = null; + vm.layout.subscribe((layout) => { + if (layout.type === "grid") setVisibleTiles = layout.setVisibleTiles; + }); + schedule(visibilityInputMarbles, { + a: () => setVisibleTiles!(Infinity), + b: () => setVisibleTiles!(3), + }); + + expectObservable(summarizeLayout(vm.layout)).toBe( + expectedLayoutMarbles, + { + a: { + type: "grid", + spotlight: undefined, + grid: ["local:0", `${aliceId}:0`, `${bobId}:0`, `${daveId}:0`], + }, + b: { + type: "grid", + spotlight: undefined, + grid: ["local:0", `${daveId}:0`, `${bobId}:0`, `${aliceId}:0`], + }, + }, + ); + }, + ); + }); +}); + test("spotlight speakers swap places", () => { - withTestScheduler(({ cold, schedule, expectObservable }) => { + withTestScheduler(({ hot, schedule, expectObservable }) => { // Go immediately into spotlight mode for the test const modeInputMarbles = " s"; // First Bob speaks, then Dave, then Alice @@ -422,11 +499,12 @@ test("spotlight speakers swap places", () => { withCallViewModel( of([aliceParticipant, bobParticipant, daveParticipant]), + of([aliceRtcMember, bobRtcMember, daveRtcMember]), of(ConnectionState.Connected), new Map([ - [aliceParticipant, cold(aSpeakingInputMarbles, { y: true, n: false })], - [bobParticipant, cold(bSpeakingInputMarbles, { y: true, n: false })], - [daveParticipant, cold(dSpeakingInputMarbles, { y: true, n: false })], + [aliceParticipant, hot(aSpeakingInputMarbles, { y: true, n: false })], + [bobParticipant, hot(bSpeakingInputMarbles, { y: true, n: false })], + [daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })], ]), (vm) => { schedule(modeInputMarbles, { s: () => vm.setGridMode("spotlight") }); @@ -470,6 +548,7 @@ test("layout enters picture-in-picture mode when requested", () => { withCallViewModel( of([aliceParticipant, bobParticipant]), + of([aliceRtcMember, bobRtcMember]), of(ConnectionState.Connected), new Map(), (vm) => { @@ -510,6 +589,7 @@ test("spotlight remembers whether it's expanded", () => { withCallViewModel( of([aliceParticipant, bobParticipant]), + of([aliceRtcMember, bobRtcMember]), of(ConnectionState.Connected), new Map(), (vm) => { @@ -554,3 +634,151 @@ test("spotlight remembers whether it's expanded", () => { ); }); }); + +test("participants must have a MatrixRTCSession to be visible", () => { + withTestScheduler(({ hot, expectObservable }) => { + // iterate through a number of combinations of participants and MatrixRTC memberships + // Bob never has an MatrixRTC membership + const scenarioInputMarbles = " abcdec"; + // Bob should never be visible + const expectedLayoutMarbles = "a-bc-b"; + + withCallViewModel( + hot(scenarioInputMarbles, { + a: [], + b: [bobParticipant], + c: [aliceParticipant, bobParticipant], + d: [aliceParticipant, daveParticipant, bobParticipant], + e: [aliceParticipant, daveParticipant, bobSharingScreen], + }), + hot(scenarioInputMarbles, { + a: [], + b: [], + c: [aliceRtcMember], + d: [aliceRtcMember, daveRtcMember], + e: [aliceRtcMember, daveRtcMember], + }), + of(ConnectionState.Connected), + new Map(), + (vm) => { + vm.setGridMode("grid"); + expectObservable(summarizeLayout(vm.layout)).toBe( + expectedLayoutMarbles, + { + a: { + type: "grid", + spotlight: undefined, + grid: ["local:0"], + }, + b: { + type: "one-on-one", + local: "local:0", + remote: `${aliceId}:0`, + }, + c: { + type: "grid", + spotlight: undefined, + grid: ["local:0", `${aliceId}:0`, `${daveId}:0`], + }, + }, + ); + }, + ); + }); +}); + +test("shows participants without MatrixRTCSession when enabled in settings", () => { + try { + // enable the setting: + showNonMemberTiles.setValue(true); + withTestScheduler(({ hot, expectObservable }) => { + const scenarioInputMarbles = " abc"; + const expectedLayoutMarbles = "abc"; + + withCallViewModel( + hot(scenarioInputMarbles, { + a: [], + b: [aliceParticipant], + c: [aliceParticipant, bobParticipant], + }), + of([]), // No one joins the MatrixRTC session + of(ConnectionState.Connected), + new Map(), + (vm) => { + vm.setGridMode("grid"); + expectObservable(summarizeLayout(vm.layout)).toBe( + expectedLayoutMarbles, + { + a: { + type: "grid", + spotlight: undefined, + grid: ["local:0"], + }, + b: { + type: "one-on-one", + local: "local:0", + remote: `${aliceId}:0`, + }, + c: { + type: "grid", + spotlight: undefined, + grid: ["local:0", `${aliceId}:0`, `${bobId}:0`], + }, + }, + ); + }, + ); + }); + } finally { + showNonMemberTiles.setValue(showNonMemberTiles.defaultValue); + } +}); + +it("should show at least one tile per MatrixRTCSession", () => { + withTestScheduler(({ hot, expectObservable }) => { + // iterate through some combinations of MatrixRTC memberships + const scenarioInputMarbles = " abcd"; + // There should always be one tile for each MatrixRTCSession + const expectedLayoutMarbles = "abcd"; + + withCallViewModel( + of([]), + hot(scenarioInputMarbles, { + a: [], + b: [aliceRtcMember], + c: [aliceRtcMember, daveRtcMember], + d: [daveRtcMember], + }), + of(ConnectionState.Connected), + new Map(), + (vm) => { + vm.setGridMode("grid"); + expectObservable(summarizeLayout(vm.layout)).toBe( + expectedLayoutMarbles, + { + a: { + type: "grid", + spotlight: undefined, + grid: ["local:0"], + }, + b: { + type: "one-on-one", + local: "local:0", + remote: `${aliceId}:0`, + }, + c: { + type: "grid", + spotlight: undefined, + grid: ["local:0", `${aliceId}:0`, `${daveId}:0`], + }, + d: { + type: "one-on-one", + local: "local:0", + remote: `${daveId}:0`, + }, + }, + ); + }, + ); + }); +}); diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 8999dc89..c701519b 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -11,23 +11,22 @@ import { observeParticipantMedia, } from "@livekit/components-core"; import { - Room as LivekitRoom, - LocalParticipant, + type Room as LivekitRoom, + type LocalParticipant, LocalVideoTrack, ParticipantEvent, - RemoteParticipant, + type RemoteParticipant, Track, } from "livekit-client"; import { - Room as MatrixRoom, - RoomMember, - RoomStateEvent, + type Room as MatrixRoom, + type RoomMember, } from "matrix-js-sdk/src/matrix"; import { + BehaviorSubject, EMPTY, - Observable, + type Observable, Subject, - audit, combineLatest, concat, distinctUntilChanged, @@ -50,32 +49,40 @@ import { withLatestFrom, } from "rxjs"; import { logger } from "matrix-js-sdk/src/logger"; +import { + type MatrixRTCSession, + MatrixRTCSessionEvent, +} from "matrix-js-sdk/src/matrixrtc"; import { ViewModel } from "./ViewModel"; import { ECAddonConnectionState, - ECConnectionState, + type ECConnectionState, } from "../livekit/useECConnectionState"; import { LocalUserMediaViewModel, - MediaViewModel, + type MediaViewModel, observeTrackReference, RemoteUserMediaViewModel, ScreenShareViewModel, - UserMediaViewModel, + type UserMediaViewModel, } from "./MediaViewModel"; import { accumulate, finalizeValue } from "../utils/observable"; import { ObservableScope } from "./ObservableScope"; -import { duplicateTiles } from "../settings/settings"; +import { duplicateTiles, showNonMemberTiles } from "../settings/settings"; import { isFirefox } from "../Platform"; import { setPipEnabled } from "../controls"; -import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel"; +import { + type GridTileViewModel, + type SpotlightTileViewModel, +} from "./TileViewModel"; import { TileStore } from "./TileStore"; import { gridLikeLayout } from "./GridLikeLayout"; import { spotlightExpandedLayout } from "./SpotlightExpandedLayout"; import { oneOnOneLayout } from "./OneOnOneLayout"; import { pipLayout } from "./PipLayout"; -import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { observeSpeaker } from "./observeSpeaker"; // How long we wait after a focus switch before showing the real participant // list again @@ -136,18 +143,21 @@ export interface GridLayout { type: "grid"; spotlight?: SpotlightTileViewModel; grid: GridTileViewModel[]; + setVisibleTiles: (value: number) => void; } export interface SpotlightLandscapeLayout { type: "spotlight-landscape"; spotlight: SpotlightTileViewModel; grid: GridTileViewModel[]; + setVisibleTiles: (value: number) => void; } export interface SpotlightPortraitLayout { type: "spotlight-portrait"; spotlight: SpotlightTileViewModel; grid: GridTileViewModel[]; + setVisibleTiles: (value: number) => void; } export interface SpotlightExpandedLayout { @@ -216,62 +226,72 @@ enum SortingBin { interface LayoutScanState { layout: Layout | null; tiles: TileStore; - visibleTiles: Set; } class UserMedia { private readonly scope = new ObservableScope(); public readonly vm: UserMediaViewModel; + private readonly participant: BehaviorSubject< + LocalParticipant | RemoteParticipant | undefined + >; + public readonly speaker: Observable; public readonly presenter: Observable; - public constructor( public readonly id: string, member: RoomMember | undefined, - participant: LocalParticipant | RemoteParticipant, + participant: LocalParticipant | RemoteParticipant | undefined, encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, ) { - this.vm = participant.isLocal - ? new LocalUserMediaViewModel( - id, - member, - participant as LocalParticipant, - encryptionSystem, - livekitRoom, - ) - : new RemoteUserMediaViewModel( - id, - member, - participant as RemoteParticipant, - encryptionSystem, - livekitRoom, - ); + this.participant = new BehaviorSubject(participant); - this.speaker = this.vm.speaking.pipe( - // Require 1 s of continuous speaking to become a speaker, and 60 s of - // continuous silence to stop being considered a speaker - audit((s) => - merge( - timer(s ? 1000 : 60000), - // If the speaking flag resets to its original value during this time, - // end the silencing window to stick with that original value - this.vm.speaking.pipe(filter((s1) => s1 !== s)), - ), + if (participant?.isLocal) { + this.vm = new LocalUserMediaViewModel( + this.id, + member, + this.participant.asObservable() as Observable, + encryptionSystem, + livekitRoom, + ); + } else { + this.vm = new RemoteUserMediaViewModel( + id, + member, + this.participant.asObservable() as Observable< + RemoteParticipant | undefined + >, + encryptionSystem, + livekitRoom, + ); + } + + this.speaker = observeSpeaker(this.vm.speaking).pipe(this.scope.state()); + + this.presenter = this.participant.pipe( + switchMap( + (p) => + (p && + observeParticipantEvents( + p, + ParticipantEvent.TrackPublished, + ParticipantEvent.TrackUnpublished, + ParticipantEvent.LocalTrackPublished, + ParticipantEvent.LocalTrackUnpublished, + ).pipe(map((p) => p.isScreenShareEnabled))) ?? + of(false), ), - startWith(false), - // Make this Observable hot so that the timers don't reset when you - // resubscribe this.scope.state(), ); + } - this.presenter = observeParticipantEvents( - participant, - ParticipantEvent.TrackPublished, - ParticipantEvent.TrackUnpublished, - ParticipantEvent.LocalTrackPublished, - ParticipantEvent.LocalTrackUnpublished, - ).pipe(map((p) => p.isScreenShareEnabled)); + public updateParticipant( + newParticipant: LocalParticipant | RemoteParticipant | undefined, + ): void { + if (this.participant.value !== newParticipant) { + // Update the BehaviourSubject in the UserMedia. + this.participant.next(newParticipant); + } } public destroy(): void { @@ -282,6 +302,9 @@ class UserMedia { class ScreenShare { public readonly vm: ScreenShareViewModel; + private readonly participant: BehaviorSubject< + LocalParticipant | RemoteParticipant + >; public constructor( id: string, @@ -290,12 +313,15 @@ class ScreenShare { encryptionSystem: EncryptionSystem, liveKitRoom: LivekitRoom, ) { + this.participant = new BehaviorSubject(participant); + this.vm = new ScreenShareViewModel( id, member, - participant, + this.participant.asObservable(), encryptionSystem, liveKitRoom, + participant.isLocal, ); } @@ -317,7 +343,7 @@ function findMatrixRoomMember( // must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon if (parts.length < 3) { logger.warn( - "Livekit participants ID doesn't look like a userId:deviceId combination", + `Livekit participants ID (${id}) doesn't look like a userId:deviceId combination`, ); return undefined; } @@ -332,11 +358,11 @@ function findMatrixRoomMember( export class CallViewModel extends ViewModel { public readonly localVideo: Observable = observeTrackReference( - this.livekitRoom.localParticipant, + of(this.livekitRoom.localParticipant), Track.Source.Camera, ).pipe( map((trackRef) => { - const track = trackRef.publication?.track; + const track = trackRef?.publication?.track; return track instanceof LocalVideoTrack ? track : null; }), ); @@ -416,49 +442,97 @@ export class CallViewModel extends ViewModel { this.remoteParticipants, observeParticipantMedia(this.livekitRoom.localParticipant), duplicateTiles.value, - // Also react to changes in the list of members - fromEvent(this.matrixRoom, RoomStateEvent.Update).pipe(startWith(null)), + // Also react to changes in the MatrixRTC session list. + // The session list will also be update if a room membership changes. + // No additional RoomState event listener needs to be set up. + fromEvent( + this.matrixRTCSession, + MatrixRTCSessionEvent.MembershipsChanged, + ).pipe(startWith(null)), + showNonMemberTiles.value, ]).pipe( scan( ( prevItems, - [remoteParticipants, { participant: localParticipant }, duplicateTiles], + [ + remoteParticipants, + { participant: localParticipant }, + duplicateTiles, + _membershipsChanged, + showNonMemberTiles, + ], ) => { const newItems = new Map( function* (this: CallViewModel): Iterable<[string, MediaItem]> { - for (const p of [localParticipant, ...remoteParticipants]) { - const id = p === localParticipant ? "local" : p.identity; - const member = findMatrixRoomMember(this.matrixRoom, id); - if (member === undefined) - logger.warn( - `Ruh, roh! No matrix member found for SFU participant '${p.identity}': creating g-g-g-ghost!`, - ); + // m.rtc.members are the basis for calculating what is visible in the call + for (const rtcMember of this.matrixRTCSession.memberships) { + const room = this.matrixRTCSession.room; + // WARN! This is not exactly the sender but the user defined in the state key. + // This will be available once we change to the new "member as object" format in the MatrixRTC object. + let livekitParticipantId = + rtcMember.sender + ":" + rtcMember.deviceId; - // Create as many tiles for this participant as called for by - // the duplicateTiles option + let participant: + | LocalParticipant + | RemoteParticipant + | undefined = undefined; + if ( + rtcMember.sender === room.client.getUserId()! && + rtcMember.deviceId === room.client.getDeviceId() + ) { + livekitParticipantId = "local"; + participant = localParticipant; + } else { + participant = remoteParticipants.find( + (p) => p.identity === livekitParticipantId, + ); + } + + const member = findMatrixRoomMember(room, livekitParticipantId); + if (!member) { + logger.error( + "Could not find member for media id: ", + livekitParticipantId, + ); + } for (let i = 0; i < 1 + duplicateTiles; i++) { - const userMediaId = `${id}:${i}`; + const indexedMediaId = `${livekitParticipantId}:${i}`; + let prevMedia = prevItems.get(indexedMediaId); + if (prevMedia && prevMedia instanceof UserMedia) { + prevMedia.updateParticipant(participant); + if (prevMedia.vm.member === undefined) { + // We have a previous media created because of the `debugShowNonMember` flag. + // In this case we actually replace the media item. + // This "hack" never occurs if we do not use the `debugShowNonMember` debugging + // option and if we always find a room member for each rtc member (which also + // only fails if we have a fundamental problem) + prevMedia = undefined; + } + } yield [ - userMediaId, - prevItems.get(userMediaId) ?? + indexedMediaId, + // We create UserMedia with or without a participant. + // This will be the initial value of a BehaviourSubject. + // Once a participant appears we will update the BehaviourSubject. (see above) + prevMedia ?? new UserMedia( - userMediaId, + indexedMediaId, member, - p, + participant, this.encryptionSystem, this.livekitRoom, ), ]; - if (p.isScreenShareEnabled) { - const screenShareId = `${userMediaId}:screen-share`; + if (participant?.isScreenShareEnabled) { + const screenShareId = `${indexedMediaId}:screen-share`; yield [ screenShareId, prevItems.get(screenShareId) ?? new ScreenShare( screenShareId, member, - p, + participant, this.encryptionSystem, this.livekitRoom, ), @@ -469,8 +543,55 @@ export class CallViewModel extends ViewModel { }.bind(this)(), ); - for (const [id, t] of prevItems) if (!newItems.has(id)) t.destroy(); - return newItems; + // Generate non member items (items without a corresponding MatrixRTC member) + // Those items should not be rendered, they are participants in LiveKit that do not have a corresponding + // MatrixRTC members. This cannot be any good: + // - A malicious user impersonates someone + // - Someone injects abusive content + // - The user cannot have encryption keys so it makes no sense to participate + // We can only trust users that have a MatrixRTC member event. + // + // This is still available as a debug option. This can be useful + // - If one wants to test scalability using the LiveKit CLI. + // - If an experimental project does not yet do the MatrixRTC bits. + // - If someone wants to debug if the LiveKit connection works but MatrixRTC room state failed to arrive. + const newNonMemberItems = showNonMemberTiles + ? new Map( + function* (this: CallViewModel): Iterable<[string, MediaItem]> { + for (const participant of remoteParticipants) { + for (let i = 0; i < 1 + duplicateTiles; i++) { + const maybeNonMemberParticipantId = + participant.identity + ":" + i; + if (!newItems.has(maybeNonMemberParticipantId)) { + const nonMemberId = maybeNonMemberParticipantId; + yield [ + nonMemberId, + prevItems.get(nonMemberId) ?? + new UserMedia( + nonMemberId, + undefined, + participant, + this.encryptionSystem, + this.livekitRoom, + ), + ]; + } + } + } + }.bind(this)(), + ) + : new Map(); + if (newNonMemberItems.size > 0) { + logger.debug("Added NonMember items: ", newNonMemberItems); + } + + const combinedNew = new Map([ + ...newNonMemberItems.entries(), + ...newItems.entries(), + ]); + + for (const [id, t] of prevItems) if (!combinedNew.has(id)) t.destroy(); + return combinedNew; }, new Map(), ), @@ -490,9 +611,17 @@ export class CallViewModel extends ViewModel { ), ); - private readonly localUserMedia: Observable = - this.mediaItems.pipe( - map((ms) => ms.find((m) => m.vm.local)!.vm as LocalUserMediaViewModel), + public readonly memberChanges = this.userMedia + .pipe(map((mediaItems) => mediaItems.map((m) => m.id))) + .pipe( + scan( + (prev, ids) => { + const left = prev.ids.filter((id) => !ids.includes(id)); + const joined = ids.filter((id) => !prev.ids.includes(id)); + return { ids, joined, left }; + }, + { ids: [], joined: [], left: [] }, + ), ); /** @@ -506,7 +635,7 @@ export class CallViewModel extends ViewModel { this.scope.state(), ); - private readonly spotlightSpeaker: Observable = + private readonly spotlightSpeaker: Observable = this.userMedia.pipe( switchMap((mediaItems) => mediaItems.length === 0 @@ -517,7 +646,7 @@ export class CallViewModel extends ViewModel { ), ), ), - scan<(readonly [UserMedia, boolean])[], UserMedia, null>( + scan<(readonly [UserMedia, boolean])[], UserMedia | undefined, null>( (prev, mediaItems) => { // Only remote users that are still in the call should be sticky const [stickyMedia, stickySpeaking] = @@ -534,11 +663,11 @@ export class CallViewModel extends ViewModel { // Otherwise, spotlight an arbitrary remote user mediaItems.find(([m]) => !m.vm.local)?.[0] ?? // Otherwise, spotlight the local user - mediaItems.find(([m]) => m.vm.local)![0]); + mediaItems.find(([m]) => m.vm.local)?.[0]); }, null, ), - map((speaker) => speaker.vm), + map((speaker) => speaker?.vm ?? null), this.scope.state(), ); @@ -578,37 +707,57 @@ export class CallViewModel extends ViewModel { }), ); - private readonly spotlightAndPip: Observable< - [Observable, Observable] - > = this.screenShares.pipe( - map((screenShares) => - screenShares.length > 0 - ? ([of(screenShares.map((m) => m.vm)), this.spotlightSpeaker] as const) - : ([ - this.spotlightSpeaker.pipe(map((speaker) => [speaker!])), - this.spotlightSpeaker.pipe( - switchMap((speaker) => - speaker.local - ? of(null) - : this.localUserMedia.pipe( - switchMap((vm) => - vm.alwaysShow.pipe( - map((alwaysShow) => (alwaysShow ? vm : null)), - ), - ), - ), - ), - ), - ] as const), - ), - ); - private readonly spotlight: Observable = - this.spotlightAndPip.pipe( - switchMap(([spotlight]) => spotlight), + this.screenShares.pipe( + switchMap((screenShares) => { + if (screenShares.length > 0) { + return of(screenShares.map((m) => m.vm)); + } + + return this.spotlightSpeaker.pipe( + map((speaker) => (speaker ? [speaker] : [])), + ); + }), this.scope.state(), ); + private readonly pip: Observable = combineLatest([ + this.screenShares, + this.spotlightSpeaker, + this.mediaItems, + ]).pipe( + switchMap(([screenShares, spotlight, mediaItems]) => { + if (screenShares.length > 0) { + return this.spotlightSpeaker; + } + if (!spotlight || spotlight.local) { + return of(null); + } + + const localUserMedia = mediaItems.find( + (m) => m.vm instanceof LocalUserMediaViewModel, + ) as UserMedia | undefined; + + const localUserMediaViewModel = localUserMedia?.vm as + | LocalUserMediaViewModel + | undefined; + + if (!localUserMediaViewModel) { + return of(null); + } + return localUserMediaViewModel.alwaysShow.pipe( + map((alwaysShow) => { + if (alwaysShow) { + return localUserMediaViewModel; + } + + return null; + }), + ); + }), + this.scope.state(), + ); + private readonly hasRemoteScreenShares: Observable = this.spotlight.pipe( map((spotlight) => @@ -617,9 +766,6 @@ export class CallViewModel extends ViewModel { distinctUntilChanged(), ); - private readonly pip: Observable = - this.spotlightAndPip.pipe(switchMap(([, pip]) => pip)); - private readonly pipEnabled: Observable = setPipEnabled.pipe( startWith(false), ); @@ -723,15 +869,16 @@ export class CallViewModel extends ViewModel { this.mediaItems.pipe( map((mediaItems) => { if (mediaItems.length !== 2) return null; - const local = mediaItems.find((vm) => vm.vm.local)! - .vm as LocalUserMediaViewModel; + const local = mediaItems.find((vm) => vm.vm.local)?.vm as + | LocalUserMediaViewModel + | undefined; const remote = mediaItems.find((vm) => !vm.vm.local)?.vm as | RemoteUserMediaViewModel | undefined; // There might not be a remote tile if there are screen shares, or if // only the local user is in the call and they're using the duplicate // tiles option - if (remote === undefined) return null; + if (!remote || !local) return null; return { type: "one-on-one", local, remote }; }), @@ -804,68 +951,74 @@ export class CallViewModel extends ViewModel { this.scope.state(), ); - /** - * The layout of tiles in the call interface. - */ - public readonly layout: Observable = this.layoutMedia.pipe( - // Each layout will produce a set of tiles, and these tiles have an - // observable indicating whether they're visible. We loop this information - // back into the layout process by using switchScan. - switchScan< - LayoutMedia, - LayoutScanState, - Observable + // There is a cyclical dependency here: the layout algorithms want to know + // which tiles are on screen, but to know which tiles are on screen we have to + // first render a layout. To deal with this we assume initially that no tiles + // are visible, and loop the data back into the layouts with a Subject. + private readonly visibleTiles = new Subject(); + private readonly setVisibleTiles = (value: number): void => + this.visibleTiles.next(value); + + public readonly layoutInternals: Observable< + LayoutScanState & { layout: Layout } + > = combineLatest([ + this.layoutMedia, + this.visibleTiles.pipe(startWith(0), distinctUntilChanged()), + ]).pipe( + scan< + [LayoutMedia, number], + LayoutScanState & { layout: Layout }, + LayoutScanState >( - ({ tiles: prevTiles, visibleTiles }, media) => { + ({ tiles: prevTiles }, [media, visibleTiles]) => { let layout: Layout; let newTiles: TileStore; switch (media.type) { case "grid": case "spotlight-landscape": case "spotlight-portrait": - [layout, newTiles] = gridLikeLayout(media, visibleTiles, prevTiles); - break; - case "spotlight-expanded": - [layout, newTiles] = spotlightExpandedLayout( + [layout, newTiles] = gridLikeLayout( media, visibleTiles, + this.setVisibleTiles, prevTiles, ); break; + case "spotlight-expanded": + [layout, newTiles] = spotlightExpandedLayout(media, prevTiles); + break; case "one-on-one": - [layout, newTiles] = oneOnOneLayout(media, visibleTiles, prevTiles); + [layout, newTiles] = oneOnOneLayout(media, prevTiles); break; case "pip": - [layout, newTiles] = pipLayout(media, visibleTiles, prevTiles); + [layout, newTiles] = pipLayout(media, prevTiles); break; } - // Take all of the 'visible' observables and combine them into one big - // observable array - const visibilities = - newTiles.gridTiles.length === 0 - ? of([]) - : combineLatest(newTiles.gridTiles.map((tile) => tile.visible)); - return visibilities.pipe( - map((visibilities) => ({ - layout: layout, - tiles: newTiles, - visibleTiles: new Set( - newTiles.gridTiles.filter((_tile, i) => visibilities[i]), - ), - })), - ); - }, - { - layout: null, - tiles: TileStore.empty(), - visibleTiles: new Set(), + return { layout, tiles: newTiles }; }, + { layout: null, tiles: TileStore.empty() }, ), + this.scope.state(), + ); + + /** + * The layout of tiles in the call interface. + */ + public readonly layout: Observable = this.layoutInternals.pipe( map(({ layout }) => layout), this.scope.state(), ); + /** + * The current generation of the tile store, exposed for debugging purposes. + */ + public readonly tileStoreGeneration: Observable = + this.layoutInternals.pipe( + map(({ tiles }) => tiles.generation), + this.scope.state(), + ); + public showSpotlightIndicators: Observable = this.layout.pipe( map((l) => l.type !== "grid"), this.scope.state(), @@ -1012,7 +1165,7 @@ export class CallViewModel extends ViewModel { public constructor( // A call is permanently tied to a single Matrix room and LiveKit room - private readonly matrixRoom: MatrixRoom, + private readonly matrixRTCSession: MatrixRTCSession, private readonly livekitRoom: LivekitRoom, private readonly encryptionSystem: EncryptionSystem, private readonly connectionState: Observable, diff --git a/src/state/GridLikeLayout.ts b/src/state/GridLikeLayout.ts index 7fcada95..e5a31cf6 100644 --- a/src/state/GridLikeLayout.ts +++ b/src/state/GridLikeLayout.ts @@ -5,9 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { Layout, LayoutMedia } from "./CallViewModel"; -import { TileStore } from "./TileStore"; -import { GridTileViewModel } from "./TileViewModel"; +import { type Layout, type LayoutMedia } from "./CallViewModel"; +import { type TileStore } from "./TileStore"; export type GridLikeLayoutType = | "grid" @@ -20,7 +19,8 @@ export type GridLikeLayoutType = */ export function gridLikeLayout( media: LayoutMedia & { type: GridLikeLayoutType }, - visibleTiles: Set, + visibleTiles: number, + setVisibleTiles: (value: number) => void, prevTiles: TileStore, ): [Layout & { type: GridLikeLayoutType }, TileStore] { const update = prevTiles.from(visibleTiles); @@ -37,6 +37,7 @@ export function gridLikeLayout( type: media.type, spotlight: tiles.spotlightTile, grid: tiles.gridTiles, + setVisibleTiles, } as Layout & { type: GridLikeLayoutType }, tiles, ]; diff --git a/src/state/MediaViewModel.test.ts b/src/state/MediaViewModel.test.ts index 5b5e59a7..c4e0bee6 100644 --- a/src/state/MediaViewModel.test.ts +++ b/src/state/MediaViewModel.test.ts @@ -8,14 +8,17 @@ Please see LICENSE in the repository root for full details. import { expect, test, vi } from "vitest"; import { + mockRtcMembership, withLocalMedia, withRemoteMedia, withTestScheduler, } from "../utils/test"; +const rtcMembership = mockRtcMembership("@alice:example.org", "AAAA"); + test("control a participant's volume", async () => { const setVolumeSpy = vi.fn(); - await withRemoteMedia({}, { setVolume: setVolumeSpy }, (vm) => + await withRemoteMedia(rtcMembership, {}, { setVolume: setVolumeSpy }, (vm) => withTestScheduler(({ expectObservable, schedule }) => { schedule("-ab---c---d|", { a() { @@ -60,7 +63,7 @@ test("control a participant's volume", async () => { }); test("toggle fit/contain for a participant's video", async () => { - await withRemoteMedia({}, {}, (vm) => + await withRemoteMedia(rtcMembership, {}, {}, (vm) => withTestScheduler(({ expectObservable, schedule }) => { schedule("-ab|", { a: () => vm.toggleFitContain(), @@ -76,17 +79,21 @@ test("toggle fit/contain for a participant's video", async () => { }); test("local media remembers whether it should always be shown", async () => { - await withLocalMedia({}, (vm) => + await withLocalMedia(rtcMembership, {}, (vm) => withTestScheduler(({ expectObservable, schedule }) => { schedule("-a|", { a: () => vm.setAlwaysShow(false) }); expectObservable(vm.alwaysShow).toBe("ab", { a: true, b: false }); }), ); // Next local media should start out *not* always shown - await withLocalMedia({}, (vm) => - withTestScheduler(({ expectObservable, schedule }) => { - schedule("-a|", { a: () => vm.setAlwaysShow(true) }); - expectObservable(vm.alwaysShow).toBe("ab", { a: false, b: true }); - }), + await withLocalMedia( + rtcMembership, + + {}, + (vm) => + withTestScheduler(({ expectObservable, schedule }) => { + schedule("-a|", { a: () => vm.setAlwaysShow(true) }); + expectObservable(vm.alwaysShow).toBe("ab", { a: false, b: true }); + }), ); }); diff --git a/src/state/MediaViewModel.ts b/src/state/MediaViewModel.ts index 36e76d38..ea015eb8 100644 --- a/src/state/MediaViewModel.ts +++ b/src/state/MediaViewModel.ts @@ -6,33 +6,32 @@ Please see LICENSE in the repository root for full details. */ import { - AudioSource, - TrackReferenceOrPlaceholder, - VideoSource, + type AudioSource, + type TrackReferenceOrPlaceholder, + type VideoSource, observeParticipantEvents, observeParticipantMedia, roomEventSelector, } from "@livekit/components-core"; import { - LocalParticipant, + type LocalParticipant, LocalTrack, - Participant, + type Participant, ParticipantEvent, - RemoteParticipant, + type RemoteParticipant, Track, TrackEvent, facingModeFromLocalTrack, - Room as LivekitRoom, + type Room as LivekitRoom, RoomEvent as LivekitRoomEvent, RemoteTrack, } from "livekit-client"; -import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix"; +import { type RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix"; import { BehaviorSubject, - Observable, + type Observable, Subject, combineLatest, - distinctUntilChanged, distinctUntilKeyChanged, filter, fromEvent, @@ -40,7 +39,6 @@ import { map, merge, of, - shareReplay, startWith, switchMap, throttleTime, @@ -51,7 +49,7 @@ import { ViewModel } from "./ViewModel"; import { useReactiveState } from "../useReactiveState"; import { alwaysShowSelf } from "../settings/settings"; import { accumulate } from "../utils/observable"; -import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; import { E2eeType } from "../e2ee/e2eeType"; // TODO: Move this naming logic into the view model @@ -77,16 +75,24 @@ export function useDisplayName(vm: MediaViewModel): string { } export function observeTrackReference( - participant: Participant, + participant: Observable, source: Track.Source, -): Observable { - return observeParticipantMedia(participant).pipe( - map(() => ({ - participant, - publication: participant.getTrackPublication(source), - source, - })), - distinctUntilKeyChanged("publication"), +): Observable { + return participant.pipe( + switchMap((p) => { + if (p) { + return observeParticipantMedia(p).pipe( + map(() => ({ + participant: p, + publication: p.getTrackPublication(source), + source, + })), + distinctUntilKeyChanged("publication"), + ); + } else { + return of(undefined); + } + }), ); } @@ -105,11 +111,11 @@ function observeRemoteTrackReceivingOkay( }; return combineLatest([ - observeTrackReference(participant, source), + observeTrackReference(of(participant), source), interval(1000).pipe(startWith(0)), ]).pipe( switchMap(async ([trackReference]) => { - const track = trackReference.publication?.track; + const track = trackReference?.publication?.track; if (!track || !(track instanceof RemoteTrack)) { return undefined; } @@ -200,14 +206,10 @@ export enum EncryptionStatus { } abstract class BaseMediaViewModel extends ViewModel { - /** - * Whether the media belongs to the local user. - */ - public readonly local = this.participant.isLocal; /** * The LiveKit video track for this media. */ - public readonly video: Observable; + public readonly video: Observable; /** * Whether there should be a warning that this media is unencrypted. */ @@ -215,6 +217,11 @@ abstract class BaseMediaViewModel extends ViewModel { public readonly encryptionStatus: Observable; + /** + * Whether this media corresponds to the local participant. + */ + public abstract readonly local: boolean; + public constructor( /** * An opaque identifier for this media. @@ -226,7 +233,12 @@ abstract class BaseMediaViewModel extends ViewModel { // TODO: Fully separate the data layer from the UI layer by keeping the // member object internal public readonly member: RoomMember | undefined, - protected readonly participant: LocalParticipant | RemoteParticipant, + // We don't necessarily have a participant if a user connects via MatrixRTC but not (yet) through + // livekit. + protected readonly participant: Observable< + LocalParticipant | RemoteParticipant | undefined + >, + encryptionSystem: EncryptionSystem, audioSource: AudioSource, videoSource: VideoSource, @@ -243,69 +255,72 @@ abstract class BaseMediaViewModel extends ViewModel { [audio, this.video], (a, v) => encryptionSystem.kind !== E2eeType.NONE && - (a.publication?.isEncrypted === false || - v.publication?.isEncrypted === false), - ).pipe( - distinctUntilChanged(), - shareReplay({ bufferSize: 1, refCount: false }), - ); + (a?.publication?.isEncrypted === false || + v?.publication?.isEncrypted === false), + ).pipe(this.scope.state()); - if (participant.isLocal || encryptionSystem.kind === E2eeType.NONE) { - this.encryptionStatus = of(EncryptionStatus.Okay).pipe( - this.scope.state(), - ); - } else if (encryptionSystem.kind === E2eeType.PER_PARTICIPANT) { - this.encryptionStatus = combineLatest([ - encryptionErrorObservable( - livekitRoom, - participant, - encryptionSystem, - "MissingKey", - ), - encryptionErrorObservable( - livekitRoom, - participant, - encryptionSystem, - "InvalidKey", - ), - observeRemoteTrackReceivingOkay(participant, audioSource), - observeRemoteTrackReceivingOkay(participant, videoSource), - ]).pipe( - map(([keyMissing, keyInvalid, audioOkay, videoOkay]) => { - if (keyMissing) return EncryptionStatus.KeyMissing; - if (keyInvalid) return EncryptionStatus.KeyInvalid; - if (audioOkay || videoOkay) return EncryptionStatus.Okay; - return undefined; // no change - }), - filter((x) => !!x), - startWith(EncryptionStatus.Connecting), - this.scope.state(), - ); - } else { - this.encryptionStatus = combineLatest([ - encryptionErrorObservable( - livekitRoom, - participant, - encryptionSystem, - "InvalidKey", - ), - observeRemoteTrackReceivingOkay(participant, audioSource), - observeRemoteTrackReceivingOkay(participant, videoSource), - ]).pipe( - map( - ([keyInvalid, audioOkay, videoOkay]): - | EncryptionStatus - | undefined => { - if (keyInvalid) return EncryptionStatus.PasswordInvalid; - if (audioOkay || videoOkay) return EncryptionStatus.Okay; - return undefined; // no change - }, - ), - filter((x) => !!x), - startWith(EncryptionStatus.Connecting), - this.scope.state(), - ); - } + this.encryptionStatus = this.participant.pipe( + switchMap((participant): Observable => { + if (!participant) { + return of(EncryptionStatus.Connecting); + } else if ( + participant.isLocal || + encryptionSystem.kind === E2eeType.NONE + ) { + return of(EncryptionStatus.Okay); + } else if (encryptionSystem.kind === E2eeType.PER_PARTICIPANT) { + return combineLatest([ + encryptionErrorObservable( + livekitRoom, + participant, + encryptionSystem, + "MissingKey", + ), + encryptionErrorObservable( + livekitRoom, + participant, + encryptionSystem, + "InvalidKey", + ), + observeRemoteTrackReceivingOkay(participant, audioSource), + observeRemoteTrackReceivingOkay(participant, videoSource), + ]).pipe( + map(([keyMissing, keyInvalid, audioOkay, videoOkay]) => { + if (keyMissing) return EncryptionStatus.KeyMissing; + if (keyInvalid) return EncryptionStatus.KeyInvalid; + if (audioOkay || videoOkay) return EncryptionStatus.Okay; + return undefined; // no change + }), + filter((x) => !!x), + startWith(EncryptionStatus.Connecting), + ); + } else { + return combineLatest([ + encryptionErrorObservable( + livekitRoom, + participant, + encryptionSystem, + "InvalidKey", + ), + observeRemoteTrackReceivingOkay(participant, audioSource), + observeRemoteTrackReceivingOkay(participant, videoSource), + ]).pipe( + map( + ([keyInvalid, audioOkay, videoOkay]): + | EncryptionStatus + | undefined => { + if (keyInvalid) return EncryptionStatus.PasswordInvalid; + if (audioOkay || videoOkay) return EncryptionStatus.Okay; + return undefined; // no change + }, + ), + filter((x) => !!x), + startWith(EncryptionStatus.Connecting), + ); + } + }), + this.scope.state(), + ); } } @@ -324,11 +339,14 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { /** * Whether the participant is speaking. */ - public readonly speaking = observeParticipantEvents( - this.participant, - ParticipantEvent.IsSpeakingChanged, - ).pipe( - map((p) => p.isSpeaking), + public readonly speaking = this.participant.pipe( + switchMap((p) => + p + ? observeParticipantEvents(p, ParticipantEvent.IsSpeakingChanged).pipe( + map((p) => p.isSpeaking), + ) + : of(false), + ), this.scope.state(), ); @@ -350,7 +368,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: LocalParticipant | RemoteParticipant, + participant: Observable, encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, ) { @@ -364,18 +382,25 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { livekitRoom, ); - const media = observeParticipantMedia(participant).pipe(this.scope.state()); + const media = participant.pipe( + switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)), + this.scope.state(), + ); this.audioEnabled = media.pipe( - map((m) => m.microphoneTrack?.isMuted === false), + map((m) => m?.microphoneTrack?.isMuted === false), ); this.videoEnabled = media.pipe( - map((m) => m.cameraTrack?.isMuted === false), + map((m) => m?.cameraTrack?.isMuted === false), ); } public toggleFitContain(): void { this._cropVideo.next(!this._cropVideo.value); } + + public get local(): boolean { + return this instanceof LocalUserMediaViewModel; + } } /** @@ -387,7 +412,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel { */ public readonly mirror = this.video.pipe( switchMap((v) => { - const track = v.publication?.track; + const track = v?.publication?.track; if (!(track instanceof LocalTrack)) return of(false); // Watch for track restarts, because they indicate a camera switch return fromEvent(track, TrackEvent.Restarted).pipe( @@ -409,7 +434,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: LocalParticipant, + participant: Observable, encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, ) { @@ -470,18 +495,17 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: RemoteParticipant, + participant: Observable, encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, ) { super(id, member, participant, encryptionSystem, livekitRoom); // Sync the local volume with LiveKit - this.localVolume - .pipe(this.scope.bind()) - .subscribe((volume) => - (this.participant as RemoteParticipant).setVolume(volume), - ); + combineLatest([ + participant, + this.localVolume.pipe(this.scope.bind()), + ]).subscribe(([p, volume]) => p && p.setVolume(volume)); } public toggleLocallyMuted(): void { @@ -504,9 +528,10 @@ export class ScreenShareViewModel extends BaseMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: LocalParticipant | RemoteParticipant, + participant: Observable, encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, + public readonly local: boolean, ) { super( id, diff --git a/src/state/ObservableScope.ts b/src/state/ObservableScope.ts index 2cf88351..5a2e0e9a 100644 --- a/src/state/ObservableScope.ts +++ b/src/state/ObservableScope.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { distinctUntilChanged, - Observable, + type Observable, shareReplay, Subject, takeUntil, diff --git a/src/state/OneOnOneLayout.ts b/src/state/OneOnOneLayout.ts index 29ed9fc0..2a0e7ff5 100644 --- a/src/state/OneOnOneLayout.ts +++ b/src/state/OneOnOneLayout.ts @@ -5,19 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { OneOnOneLayout, OneOnOneLayoutMedia } from "./CallViewModel"; -import { TileStore } from "./TileStore"; -import { GridTileViewModel } from "./TileViewModel"; +import { type OneOnOneLayout, type OneOnOneLayoutMedia } from "./CallViewModel"; +import { type TileStore } from "./TileStore"; /** * Produces a one-on-one layout with the given media. */ export function oneOnOneLayout( media: OneOnOneLayoutMedia, - visibleTiles: Set, prevTiles: TileStore, ): [OneOnOneLayout, TileStore] { - const update = prevTiles.from(visibleTiles); + const update = prevTiles.from(2); update.registerGridTile(media.local); update.registerGridTile(media.remote); const tiles = update.build(); diff --git a/src/state/PipLayout.ts b/src/state/PipLayout.ts index 35edeefe..ad56cdd5 100644 --- a/src/state/PipLayout.ts +++ b/src/state/PipLayout.ts @@ -5,19 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { PipLayout, PipLayoutMedia } from "./CallViewModel"; -import { TileStore } from "./TileStore"; -import { GridTileViewModel } from "./TileViewModel"; +import { type PipLayout, type PipLayoutMedia } from "./CallViewModel"; +import { type TileStore } from "./TileStore"; /** * Produces a picture-in-picture layout with the given media. */ export function pipLayout( media: PipLayoutMedia, - visibleTiles: Set, prevTiles: TileStore, ): [PipLayout, TileStore] { - const update = prevTiles.from(visibleTiles); + const update = prevTiles.from(0); update.registerSpotlight(media.spotlight, true); const tiles = update.build(); return [ diff --git a/src/state/SpotlightExpandedLayout.ts b/src/state/SpotlightExpandedLayout.ts index 83c5a95e..c14b24a7 100644 --- a/src/state/SpotlightExpandedLayout.ts +++ b/src/state/SpotlightExpandedLayout.ts @@ -6,21 +6,19 @@ Please see LICENSE in the repository root for full details. */ import { - SpotlightExpandedLayout, - SpotlightExpandedLayoutMedia, + type SpotlightExpandedLayout, + type SpotlightExpandedLayoutMedia, } from "./CallViewModel"; -import { TileStore } from "./TileStore"; -import { GridTileViewModel } from "./TileViewModel"; +import { type TileStore } from "./TileStore"; /** * Produces an expanded spotlight layout with the given media. */ export function spotlightExpandedLayout( media: SpotlightExpandedLayoutMedia, - visibleTiles: Set, prevTiles: TileStore, ): [SpotlightExpandedLayout, TileStore] { - const update = prevTiles.from(visibleTiles); + const update = prevTiles.from(1); update.registerSpotlight(media.spotlight, true); if (media.pip !== undefined) update.registerGridTile(media.pip); const tiles = update.build(); diff --git a/src/state/TileStore.ts b/src/state/TileStore.ts index 0288830c..cd269944 100644 --- a/src/state/TileStore.ts +++ b/src/state/TileStore.ts @@ -6,10 +6,19 @@ Please see LICENSE in the repository root for full details. */ import { BehaviorSubject } from "rxjs"; +import { logger } from "matrix-js-sdk/src/logger"; -import { MediaViewModel, UserMediaViewModel } from "./MediaViewModel"; +import { type MediaViewModel, type UserMediaViewModel } from "./MediaViewModel"; import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel"; import { fillGaps } from "../utils/iter"; +import { debugTileLayout } from "../settings/settings"; + +function debugEntries(entries: GridTileData[]): string[] { + return entries.map((e) => e.media.member?.rawDisplayName ?? "[👻]"); +} + +let DEBUG_ENABLED = false; +debugTileLayout.value.subscribe((value) => (DEBUG_ENABLED = value)); class SpotlightTileData { private readonly media_: BehaviorSubject; @@ -69,6 +78,10 @@ export class TileStore { private constructor( private readonly spotlight: SpotlightTileData | null, private readonly grid: GridTileData[], + /** + * A number incremented on each update, just for debugging purposes. + */ + public readonly generation: number, ) {} public readonly spotlightTile = this.spotlight?.vm; @@ -81,19 +94,20 @@ export class TileStore { * Creates an an empty collection of tiles. */ public static empty(): TileStore { - return new TileStore(null, []); + return new TileStore(null, [], 0); } /** * Creates a builder which can be used to update the collection, passing * ownership of the tiles to the updated collection. */ - public from(visibleTiles: Set): TileStoreBuilder { + public from(visibleTiles: number): TileStoreBuilder { return new TileStoreBuilder( this.spotlight, this.grid, - (spotlight, grid) => new TileStore(spotlight, grid), + (spotlight, grid) => new TileStore(spotlight, grid, this.generation + 1), visibleTiles, + this.generation, ); } } @@ -132,7 +146,11 @@ export class TileStoreBuilder { spotlight: SpotlightTileData | null, grid: GridTileData[], ) => TileStore, - private readonly visibleTiles: Set, + private readonly visibleTiles: number, + /** + * A number incremented on each update, just for debugging purposes. + */ + private readonly generation: number, ) {} /** @@ -140,6 +158,11 @@ export class TileStoreBuilder { * will be no spotlight tile. */ public registerSpotlight(media: MediaViewModel[], maximised: boolean): void { + if (DEBUG_ENABLED) + logger.debug( + `[TileStore, ${this.generation}] register spotlight: ${media.map((m) => m.member?.rawDisplayName ?? "[👻]")}`, + ); + if (this.spotlight !== null) throw new Error("Spotlight already set"); if (this.numGridEntries > 0) throw new Error("Spotlight must be registered before grid tiles"); @@ -159,6 +182,11 @@ export class TileStoreBuilder { * media, then that media will have no grid tile. */ public registerGridTile(media: UserMediaViewModel): void { + if (DEBUG_ENABLED) + logger.debug( + `[TileStore, ${this.generation}] register grid tile: ${media.member?.rawDisplayName ?? "[👻]"}`, + ); + if (this.spotlight !== null) { // We actually *don't* want spotlight speakers to appear in both the // spotlight and the grid, so they're filtered out here @@ -176,10 +204,8 @@ export class TileStoreBuilder { const prev = this.prevGridByMedia.get(this.spotlight.media[0]); if (prev !== undefined) { const [entry, prevIndex] = prev; - const previouslyVisible = this.visibleTiles.has(entry.vm); - const nowVisible = this.visibleTiles.has( - this.prevGrid[this.numGridEntries]?.vm, - ); + const previouslyVisible = prevIndex < this.visibleTiles; + const nowVisible = this.numGridEntries < this.visibleTiles; // If it doesn't need to move between the visible/invisible sections of // the grid, then we can keep it where it was and swap the media @@ -208,17 +234,15 @@ export class TileStoreBuilder { const prev = this.prevGridByMedia.get(media); if (prev === undefined) { // Create a new tile - (this.visibleTiles.has(this.prevGrid[this.numGridEntries]?.vm) + (this.numGridEntries < this.visibleTiles ? this.visibleGridEntries : this.invisibleGridEntries ).push(new GridTileData(media)); } else { // Reuse the existing tile const [entry, prevIndex] = prev; - const previouslyVisible = this.visibleTiles.has(entry.vm); - const nowVisible = this.visibleTiles.has( - this.prevGrid[this.numGridEntries]?.vm, - ); + const previouslyVisible = prevIndex < this.visibleTiles; + const nowVisible = this.numGridEntries < this.visibleTiles; // If it doesn't need to move between the visible/invisible sections of // the grid, then we can keep it exactly where it was previously if (previouslyVisible === nowVisible) @@ -246,6 +270,20 @@ export class TileStoreBuilder { ...this.invisibleGridEntries, ]), ]; + if (DEBUG_ENABLED) { + logger.debug( + `[TileStore, ${this.generation}] stationary: ${debugEntries(this.stationaryGridEntries)}`, + ); + logger.debug( + `[TileStore, ${this.generation}] visible: ${debugEntries(this.visibleGridEntries)}`, + ); + logger.debug( + `[TileStore, ${this.generation}] invisible: ${debugEntries(this.invisibleGridEntries)}`, + ); + logger.debug( + `[TileStore, ${this.generation}] result: ${debugEntries(grid)}`, + ); + } // Destroy unused tiles if (this.spotlight === null && this.prevSpotlight !== null) diff --git a/src/state/TileViewModel.ts b/src/state/TileViewModel.ts index 3c25907e..612d7033 100644 --- a/src/state/TileViewModel.ts +++ b/src/state/TileViewModel.ts @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { BehaviorSubject, Observable } from "rxjs"; +import { type Observable } from "rxjs"; import { ViewModel } from "./ViewModel"; -import { MediaViewModel, UserMediaViewModel } from "./MediaViewModel"; +import { type MediaViewModel, type UserMediaViewModel } from "./MediaViewModel"; let nextId = 0; function createId(): string { @@ -18,14 +18,6 @@ function createId(): string { export class GridTileViewModel extends ViewModel { public readonly id = createId(); - private readonly visible_ = new BehaviorSubject(false); - /** - * Whether the tile is visible within the current viewport. - */ - public readonly visible: Observable = this.visible_; - - public setVisible = (value: boolean): void => this.visible_.next(value); - public constructor(public readonly media: Observable) { super(); } diff --git a/src/state/observeSpeaker.test.ts b/src/state/observeSpeaker.test.ts new file mode 100644 index 00000000..daa5f033 --- /dev/null +++ b/src/state/observeSpeaker.test.ts @@ -0,0 +1,119 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { describe, test } from "vitest"; + +import { withTestScheduler } from "../utils/test"; +import { observeSpeaker } from "./observeSpeaker"; + +const yesNo = { + y: true, + n: false, +}; + +describe("observeSpeaker", () => { + describe("does not activate", () => { + const expectedOutputMarbles = "n"; + test("starts correctly", () => { + // should default to false when no input is given + const speakingInputMarbles = ""; + withTestScheduler(({ hot, expectObservable }) => { + expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( + expectedOutputMarbles, + yesNo, + ); + }); + }); + + test("after no speaking", () => { + const speakingInputMarbles = "n"; + withTestScheduler(({ hot, expectObservable }) => { + expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( + expectedOutputMarbles, + yesNo, + ); + }); + }); + + test("with speaking for 1ms", () => { + const speakingInputMarbles = "y n"; + withTestScheduler(({ hot, expectObservable }) => { + expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( + expectedOutputMarbles, + yesNo, + ); + }); + }); + + test("with speaking for 999ms", () => { + const speakingInputMarbles = "y 999ms n"; + withTestScheduler(({ hot, expectObservable }) => { + expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( + expectedOutputMarbles, + yesNo, + ); + }); + }); + + test("with speaking intermittently", () => { + const speakingInputMarbles = + "y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n"; + withTestScheduler(({ hot, expectObservable }) => { + expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( + expectedOutputMarbles, + yesNo, + ); + }); + }); + + test("with consecutive speaking then stops speaking", () => { + const speakingInputMarbles = "y y y y y y y y y y n"; + withTestScheduler(({ hot, expectObservable }) => { + expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( + expectedOutputMarbles, + yesNo, + ); + }); + }); + }); + + describe("activates", () => { + test("after 1s", () => { + // this will active after 1s as no `n` follows it: + const speakingInputMarbles = " y"; + const expectedOutputMarbles = "n 999ms y"; + withTestScheduler(({ hot, expectObservable }) => { + expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( + expectedOutputMarbles, + yesNo, + ); + }); + }); + + test("speaking for 1001ms activates for 60s", () => { + const speakingInputMarbles = " y 1s n "; + const expectedOutputMarbles = "n 999ms y 60s n"; + withTestScheduler(({ hot, expectObservable }) => { + expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( + expectedOutputMarbles, + yesNo, + ); + }); + }); + + test("speaking for 5s activates for 64s", () => { + const speakingInputMarbles = " y 5s n "; + const expectedOutputMarbles = "n 999ms y 64s n"; + withTestScheduler(({ hot, expectObservable }) => { + expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( + expectedOutputMarbles, + yesNo, + ); + }); + }); + }); +}); diff --git a/src/state/observeSpeaker.ts b/src/state/observeSpeaker.ts new file mode 100644 index 00000000..cce43ef9 --- /dev/null +++ b/src/state/observeSpeaker.ts @@ -0,0 +1,36 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ +import { + type Observable, + audit, + merge, + timer, + filter, + startWith, + distinctUntilChanged, +} from "rxjs"; + +/** + * Require 1 second of continuous speaking to become a speaker, and 60 second of + * continuous silence to stop being considered a speaker + */ +export function observeSpeaker( + isSpeakingObservable: Observable, +): Observable { + const distinct = isSpeakingObservable.pipe(distinctUntilChanged()); + + return distinct.pipe( + // Either change to the new value after the timer or re-emit the same value if it toggles back + // (audit will return the latest (toggled back) value) before the timeout. + audit((s) => + merge(timer(s ? 1000 : 60000), distinct.pipe(filter((s1) => s1 !== s))), + ), + // Filter the re-emissions (marked as: | ) that happen if we toggle quickly (<1s) from false->true->false|->.. + startWith(false), + distinctUntilChanged(), + ); +} diff --git a/src/tabs/Tabs.tsx b/src/tabs/Tabs.tsx index 8f063a97..287be30d 100644 --- a/src/tabs/Tabs.tsx +++ b/src/tabs/Tabs.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { Key, ReactNode, useId } from "react"; +import { type Key, type ReactNode, useId } from "react"; import { NavBar, NavItem } from "@vector-im/compound-web"; import styles from "./Tabs.module.css"; diff --git a/src/tile/GridTile.test.tsx b/src/tile/GridTile.test.tsx index 9b03a5ea..d7edf3b3 100644 --- a/src/tile/GridTile.test.tsx +++ b/src/tile/GridTile.test.tsx @@ -5,15 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { RemoteTrackPublication } from "livekit-client"; +import { type RemoteTrackPublication } from "livekit-client"; import { test, expect } from "vitest"; import { render, screen } from "@testing-library/react"; import { axe } from "vitest-axe"; import { of } from "rxjs"; -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { GridTile } from "./GridTile"; -import { withRemoteMedia } from "../utils/test"; +import { mockRtcMembership, withRemoteMedia } from "../utils/test"; import { GridTileViewModel } from "../state/TileViewModel"; import { ReactionsProvider } from "../useReactions"; @@ -25,6 +25,7 @@ global.IntersectionObserver = class MockIntersectionObserver { test("GridTile is accessible", async () => { await withRemoteMedia( + mockRtcMembership("@alice:example.org", "AAAA"), { rawDisplayName: "Alice", getMxcAvatarUrl: () => "mxc://adfsg", diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index 27695b65..73c17527 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -6,14 +6,14 @@ Please see LICENSE in the repository root for full details. */ import { - ComponentProps, - ReactNode, + type ComponentProps, + type ReactNode, forwardRef, useCallback, useRef, useState, } from "react"; -import { animated } from "@react-spring/web"; +import { type animated } from "@react-spring/web"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; import { @@ -38,18 +38,18 @@ import { useObservableEagerState } from "observable-hooks"; import styles from "./GridTile.module.css"; import { - UserMediaViewModel, + type UserMediaViewModel, useDisplayName, LocalUserMediaViewModel, - RemoteUserMediaViewModel, + type RemoteUserMediaViewModel, } from "../state/MediaViewModel"; import { Slider } from "../Slider"; import { MediaView } from "./MediaView"; import { useLatest } from "../useLatest"; -import { GridTileViewModel } from "../state/TileViewModel"; +import { type GridTileViewModel } from "../state/TileViewModel"; import { useMergedRefs } from "../useMergedRefs"; import { useReactions } from "../useReactions"; -import { ReactionOption } from "../reactions"; +import { type ReactionOption } from "../reactions"; interface TileProps { className?: string; @@ -175,6 +175,7 @@ const UserMediaTile = forwardRef( raisedHandTime={handRaised} currentReaction={currentReaction} raisedHandOnClick={raisedHandOnClick} + localParticipant={vm.local} {...props} /> ); diff --git a/src/tile/MediaView.module.css b/src/tile/MediaView.module.css index 3ed6c83d..70d6fead 100644 --- a/src/tile/MediaView.module.css +++ b/src/tile/MediaView.module.css @@ -15,7 +15,7 @@ Please see LICENSE in the repository root for full details. inline-size: 100%; block-size: 100%; object-fit: contain; - background-color: var(--cpd-color-bg-subtle-primary); + background-color: var(--video-tile-background); /* This transform is a no-op, but it forces Firefox to use a different rendering path, one that actually clips the corners of
-
+
( /> )}
+ {!video && !localParticipant && ( +
+ {t("video_tile.waiting_for_media")} +
+ )} {/* TODO: Bring this back once encryption status is less broken */} {/*encryptionStatus !== EncryptionStatus.Okay && (
@@ -133,7 +142,13 @@ export const MediaView = forwardRef( )*/}
{nameTagLeadingIcon} - + {displayName} {unencryptedWarning && ( @@ -146,6 +161,8 @@ export const MediaView = forwardRef( width={20} height={20} className={styles.errorIcon} + role="img" + aria-label={t("common.unencrypted")} /> )} diff --git a/src/tile/SpotlightTile.test.tsx b/src/tile/SpotlightTile.test.tsx index cedeea62..29b574a2 100644 --- a/src/tile/SpotlightTile.test.tsx +++ b/src/tile/SpotlightTile.test.tsx @@ -12,7 +12,11 @@ import userEvent from "@testing-library/user-event"; import { of } from "rxjs"; import { SpotlightTile } from "./SpotlightTile"; -import { withLocalMedia, withRemoteMedia } from "../utils/test"; +import { + mockRtcMembership, + withLocalMedia, + withRemoteMedia, +} from "../utils/test"; import { SpotlightTileViewModel } from "../state/TileViewModel"; global.IntersectionObserver = class MockIntersectionObserver { @@ -22,6 +26,7 @@ global.IntersectionObserver = class MockIntersectionObserver { test("SpotlightTile is accessible", async () => { await withRemoteMedia( + mockRtcMembership("@alice:example.org", "AAAA"), { rawDisplayName: "Alice", getMxcAvatarUrl: () => "mxc://adfsg", @@ -29,6 +34,7 @@ test("SpotlightTile is accessible", async () => { {}, async (vm1) => { await withLocalMedia( + mockRtcMembership("@bob:example.org", "BBBB"), { rawDisplayName: "Bob", getMxcAvatarUrl: () => "mxc://dlskf", diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 1c85df92..a1c3d46f 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -6,8 +6,8 @@ Please see LICENSE in the repository root for full details. */ import { - ComponentProps, - RefAttributes, + type ComponentProps, + type RefAttributes, forwardRef, useCallback, useEffect, @@ -21,40 +21,41 @@ import { ChevronRightIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { animated } from "@react-spring/web"; -import { Observable, map } from "rxjs"; +import { type Observable, map } from "rxjs"; import { useObservableEagerState, useObservableRef } from "observable-hooks"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; -import { TrackReferenceOrPlaceholder } from "@livekit/components-core"; -import { RoomMember } from "matrix-js-sdk/src/matrix"; +import { type TrackReferenceOrPlaceholder } from "@livekit/components-core"; +import { type RoomMember } from "matrix-js-sdk/src/matrix"; import { MediaView } from "./MediaView"; import styles from "./SpotlightTile.module.css"; import { - EncryptionStatus, + type EncryptionStatus, LocalUserMediaViewModel, - MediaViewModel, + type MediaViewModel, ScreenShareViewModel, - UserMediaViewModel, + type UserMediaViewModel, useDisplayName, } from "../state/MediaViewModel"; import { useInitial } from "../useInitial"; import { useMergedRefs } from "../useMergedRefs"; import { useReactiveState } from "../useReactiveState"; import { useLatest } from "../useLatest"; -import { SpotlightTileViewModel } from "../state/TileViewModel"; +import { type SpotlightTileViewModel } from "../state/TileViewModel"; interface SpotlightItemBaseProps { className?: string; "data-id": string; targetWidth: number; targetHeight: number; - video: TrackReferenceOrPlaceholder; + video: TrackReferenceOrPlaceholder | undefined; member: RoomMember | undefined; unencryptedWarning: boolean; encryptionStatus: EncryptionStatus; displayName: string; "aria-hidden"?: boolean; + localParticipant: boolean; } interface SpotlightUserMediaItemBaseProps extends SpotlightItemBaseProps { @@ -163,6 +164,7 @@ const SpotlightItem = forwardRef( displayName, encryptionStatus, "aria-hidden": ariaHidden, + localParticipant: vm.local, }; return vm instanceof ScreenShareViewModel ? ( @@ -210,7 +212,9 @@ export const SpotlightTile = forwardRef( const ref = useMergedRefs(ourRef, theirRef); const maximised = useObservableEagerState(vm.maximised); const media = useObservableEagerState(vm.media); - const [visibleId, setVisibleId] = useState(media[0].id); + const [visibleId, setVisibleId] = useState( + media[0]?.id, + ); const latestMedia = useLatest(media); const latestVisibleId = useLatest(visibleId); const visibleIndex = media.findIndex((vm) => vm.id === visibleId); diff --git a/src/useAudioContext.test.tsx b/src/useAudioContext.test.tsx new file mode 100644 index 00000000..2fda4add --- /dev/null +++ b/src/useAudioContext.test.tsx @@ -0,0 +1,133 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { expect, test, vitest } from "vitest"; +import { type FC } from "react"; +import { render } from "@testing-library/react"; +import { afterEach } from "node:test"; +import userEvent from "@testing-library/user-event"; + +import { deviceStub, MediaDevicesContext } from "./livekit/MediaDevicesContext"; +import { useAudioContext } from "./useAudioContext"; +import { soundEffectVolumeSetting } from "./settings/settings"; + +const staticSounds = Promise.resolve({ + aSound: new ArrayBuffer(0), +}); + +const TestComponent: FC = () => { + const audioCtx = useAudioContext({ + sounds: staticSounds, + latencyHint: "balanced", + }); + if (!audioCtx) { + return null; + } + return ( + <> + + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any*/} + + + ); +}; + +class MockAudioContext { + public static testContext: MockAudioContext; + + public constructor() { + MockAudioContext.testContext = this; + } + + public gain = vitest.mocked( + { + connect: () => {}, + gain: { + setValueAtTime: vitest.fn(), + }, + }, + true, + ); + + public setSinkId = vitest.fn().mockResolvedValue(undefined); + public decodeAudioData = vitest.fn().mockReturnValue(1); + public createBufferSource = vitest.fn().mockReturnValue( + vitest.mocked({ + connect: (v: unknown) => v, + start: () => {}, + addEventListener: (_name: string, cb: () => void) => cb(), + }), + ); + public createGain = vitest.fn().mockReturnValue(this.gain); + public close = vitest.fn().mockResolvedValue(undefined); +} + +afterEach(() => { + vitest.unstubAllGlobals(); +}); + +test("can play a single sound", async () => { + const user = userEvent.setup(); + vitest.stubGlobal("AudioContext", MockAudioContext); + const { findByText } = render(); + await user.click(await findByText("Valid sound")); + expect( + MockAudioContext.testContext.createBufferSource, + ).toHaveBeenCalledOnce(); +}); +test("will ignore sounds that are not registered", async () => { + const user = userEvent.setup(); + vitest.stubGlobal("AudioContext", MockAudioContext); + const { findByText } = render(); + await user.click(await findByText("Invalid sound")); + expect( + MockAudioContext.testContext.createBufferSource, + ).not.toHaveBeenCalled(); +}); + +test("will use the correct device", () => { + vitest.stubGlobal("AudioContext", MockAudioContext); + render( + {}, + }, + videoInput: deviceStub, + startUsingDeviceNames: () => {}, + stopUsingDeviceNames: () => {}, + }} + > + + , + ); + expect( + MockAudioContext.testContext.createBufferSource, + ).not.toHaveBeenCalled(); + expect(MockAudioContext.testContext.setSinkId).toHaveBeenCalledWith( + "chosen-device", + ); +}); + +test("will use the correct volume level", async () => { + const user = userEvent.setup(); + vitest.stubGlobal("AudioContext", MockAudioContext); + soundEffectVolumeSetting.setValue(0.33); + const { findByText } = render(); + await user.click(await findByText("Valid sound")); + expect( + MockAudioContext.testContext.gain.gain.setValueAtTime, + ).toHaveBeenCalledWith(0.33, 0); +}); diff --git a/src/useAudioContext.tsx b/src/useAudioContext.tsx new file mode 100644 index 00000000..656b7460 --- /dev/null +++ b/src/useAudioContext.tsx @@ -0,0 +1,127 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; +import { useState, useEffect } from "react"; + +import { + soundEffectVolumeSetting as effectSoundVolumeSetting, + useSetting, +} from "./settings/settings"; +import { useMediaDevices } from "./livekit/MediaDevicesContext"; +import { type PrefetchedSounds } from "./soundUtils"; + +/** + * Play a sound though a given AudioContext. Will take + * care of connecting the correct buffer and gating + * through gain. + * @param volume The volume to play at. + * @param ctx The context to play through. + * @param buffer The buffer to play. + * @returns A promise that resolves when the sound has finished playing. + */ +async function playSound( + ctx: AudioContext, + buffer: AudioBuffer, + volume: number, +): Promise { + const gain = ctx.createGain(); + gain.gain.setValueAtTime(volume, 0); + const src = ctx.createBufferSource(); + src.buffer = buffer; + src.connect(gain).connect(ctx.destination); + const p = new Promise((r) => src.addEventListener("ended", () => r())); + src.start(); + return p; +} + +interface Props { + /** + * The sounds to play. If no sounds should be played then + * this can be set to null, which will prevent the audio + * context from being created. + */ + sounds: PrefetchedSounds | null; + latencyHint: AudioContextLatencyCategory; +} + +interface UseAudioContext { + playSound(soundName: S): Promise; +} + +/** + * Add an audio context which can be used to play + * a set of preloaded sounds. + * @param props + * @returns Either an instance that can be used to play sounds, or null if not ready. + */ +export function useAudioContext( + props: Props, +): UseAudioContext | null { + const [effectSoundVolume] = useSetting(effectSoundVolumeSetting); + const devices = useMediaDevices(); + const [audioContext, setAudioContext] = useState(); + const [audioBuffers, setAudioBuffers] = useState>(); + + useEffect(() => { + const sounds = props.sounds; + if (!sounds) { + return; + } + const ctx = new AudioContext({ + // We want low latency for these effects. + latencyHint: props.latencyHint, + }); + + // We want to clone the content of our preloaded + // sound buffers into this context. The context may + // close during this process, so it's okay if it throws. + (async (): Promise => { + const buffers: Record = {}; + for (const [name, buffer] of Object.entries(await sounds)) { + const audioBuffer = await ctx.decodeAudioData(buffer.slice(0)); + buffers[name] = audioBuffer; + } + setAudioBuffers(buffers as Record); + })().catch((ex) => { + logger.debug("Failed to setup audio context", ex); + }); + + setAudioContext(ctx); + return (): void => { + void ctx.close().catch((ex) => { + logger.debug("Failed to close audio engine", ex); + }); + setAudioContext(undefined); + }; + }, [props.sounds, props.latencyHint]); + + // Update the sink ID whenever we change devices. + useEffect(() => { + if (audioContext && "setSinkId" in audioContext) { + // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId + // @ts-expect-error - setSinkId doesn't exist yet in types, maybe because it's not supported everywhere. + audioContext.setSinkId(devices.audioOutput.selectedId).catch((ex) => { + logger.warn("Unable to change sink for audio context", ex); + }); + } + }, [audioContext, devices]); + + // Don't return a function until we're ready. + if (!audioContext || !audioBuffers) { + return null; + } + return { + playSound: async (name): Promise => { + if (!audioBuffers[name]) { + logger.debug(`Tried to play a sound that wasn't buffered (${name})`); + return; + } + return playSound(audioContext, audioBuffers[name], effectSoundVolume); + }, + }; +} diff --git a/src/useCallViewKeyboardShortcuts.test.tsx b/src/useCallViewKeyboardShortcuts.test.tsx index 9b8d45e7..8c25bb57 100644 --- a/src/useCallViewKeyboardShortcuts.test.tsx +++ b/src/useCallViewKeyboardShortcuts.test.tsx @@ -6,13 +6,17 @@ Please see LICENSE in the repository root for full details. */ import { render } from "@testing-library/react"; -import { FC, useRef } from "react"; +import { type FC, useRef } from "react"; import { expect, test, vi } from "vitest"; import { Button } from "@vector-im/compound-web"; import userEvent from "@testing-library/user-event"; import { useCallViewKeyboardShortcuts } from "../src/useCallViewKeyboardShortcuts"; -import { ReactionOption, ReactionSet, ReactionsRowSize } from "./reactions"; +import { + type ReactionOption, + ReactionSet, + ReactionsRowSize, +} from "./reactions"; // Test Explanation: // - The main objective is to test `useCallViewKeyboardShortcuts`. @@ -93,6 +97,16 @@ test("reactions can be sent via keyboard presses", async () => { } }); +test("reaction is not sent when modifier key is held", async () => { + const user = userEvent.setup(); + + const sendReaction = vi.fn(); + render(); + + await user.keyboard("{Meta>}1{/Meta}"); + expect(sendReaction).not.toHaveBeenCalled(); +}); + test("raised hand can be sent via keyboard presses", async () => { const user = userEvent.setup(); diff --git a/src/useCallViewKeyboardShortcuts.ts b/src/useCallViewKeyboardShortcuts.ts index 7c27e1e2..a426be58 100644 --- a/src/useCallViewKeyboardShortcuts.ts +++ b/src/useCallViewKeyboardShortcuts.ts @@ -5,10 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { RefObject, useCallback, useMemo, useRef } from "react"; +import { type RefObject, useCallback, useMemo, useRef } from "react"; import { useEventTarget } from "./useEvents"; -import { ReactionOption, ReactionSet, ReactionsRowSize } from "./reactions"; +import { + type ReactionOption, + ReactionSet, + ReactionsRowSize, +} from "./reactions"; /** * Determines whether focus is in the same part of the tree as the given @@ -43,6 +47,8 @@ export function useCallViewKeyboardShortcuts( (event: KeyboardEvent) => { if (focusElement.current === null) return; if (!mayReceiveKeyEvents(focusElement.current)) return; + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) + return; if (event.key === "m") { event.preventDefault(); diff --git a/src/useLatest.ts b/src/useLatest.ts index 6a7ec41a..c54eb6c4 100644 --- a/src/useLatest.ts +++ b/src/useLatest.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { RefObject, useRef } from "react"; +import { type RefObject, useRef } from "react"; export interface LatestRef extends RefObject { current: T; diff --git a/src/useMatrixRTCSessionJoinState.ts b/src/useMatrixRTCSessionJoinState.ts index 42fed070..0bdaa25d 100644 --- a/src/useMatrixRTCSessionJoinState.ts +++ b/src/useMatrixRTCSessionJoinState.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { logger } from "matrix-js-sdk/src/logger"; import { - MatrixRTCSession, + type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { useCallback, useEffect, useState } from "react"; diff --git a/src/useMatrixRTCSessionMemberships.ts b/src/useMatrixRTCSessionMemberships.ts index 942a2257..fa9e8f46 100644 --- a/src/useMatrixRTCSessionMemberships.ts +++ b/src/useMatrixRTCSessionMemberships.ts @@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details. */ import { logger } from "matrix-js-sdk/src/logger"; -import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership"; +import { type CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership"; import { - MatrixRTCSession, + type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { useCallback, useEffect, useState } from "react"; diff --git a/src/useMergedRefs.ts b/src/useMergedRefs.ts index 20627b30..03093b77 100644 --- a/src/useMergedRefs.ts +++ b/src/useMergedRefs.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { MutableRefObject, RefCallback, useCallback } from "react"; +import { type MutableRefObject, type RefCallback, useCallback } from "react"; /** * Combines multiple refs into one, useful for attaching multiple refs to the diff --git a/src/useReactions.test.tsx b/src/useReactions.test.tsx index 6140793f..56edd7e1 100644 --- a/src/useReactions.test.tsx +++ b/src/useReactions.test.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { render } from "@testing-library/react"; -import { act, FC } from "react"; +import { act, type FC } from "react"; import { describe, expect, test } from "vitest"; import { RoomEvent } from "matrix-js-sdk/src/matrix"; diff --git a/src/useReactions.tsx b/src/useReactions.tsx index 7195cfd0..7289187a 100644 --- a/src/useReactions.tsx +++ b/src/useReactions.tsx @@ -7,31 +7,31 @@ Please see LICENSE in the repository root for full details. import { EventType, - MatrixEvent, + type MatrixEvent, RelationType, RoomEvent as MatrixRoomEvent, MatrixEventEvent, } from "matrix-js-sdk/src/matrix"; -import { ReactionEventContent } from "matrix-js-sdk/src/types"; +import { type ReactionEventContent } from "matrix-js-sdk/src/types"; import { createContext, useContext, useState, - ReactNode, + type ReactNode, useCallback, useEffect, useMemo, } from "react"; -import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { logger } from "matrix-js-sdk/src/logger"; import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships"; import { useClientState } from "./ClientContext"; import { - ECallReactionEventContent, + type ECallReactionEventContent, ElementCallReactionEventType, GenericReaction, - ReactionOption, + type ReactionOption, ReactionSet, } from "./reactions"; import { useLatest } from "./useLatest"; diff --git a/src/useReactiveState.ts b/src/useReactiveState.ts index 76d8d410..2a58d33a 100644 --- a/src/useReactiveState.ts +++ b/src/useReactiveState.ts @@ -6,9 +6,9 @@ Please see LICENSE in the repository root for full details. */ import { - DependencyList, - Dispatch, - SetStateAction, + type DependencyList, + type Dispatch, + type SetStateAction, useCallback, useRef, useState, diff --git a/src/useTheme.test.ts b/src/useTheme.test.ts index 31822668..d0927b35 100644 --- a/src/useTheme.test.ts +++ b/src/useTheme.test.ts @@ -11,7 +11,7 @@ import { beforeEach, describe, expect, - Mock, + type Mock, test, vi, } from "vitest"; diff --git a/src/utils/matrix.ts b/src/utils/matrix.ts index d3821a3f..abc49295 100644 --- a/src/utils/matrix.ts +++ b/src/utils/matrix.ts @@ -9,12 +9,12 @@ import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb"; import { MemoryStore } from "matrix-js-sdk/src/store/memory"; import { createClient, - ICreateClientOpts, + type ICreateClientOpts, Preset, Visibility, } from "matrix-js-sdk/src/matrix"; import { ClientEvent } from "matrix-js-sdk/src/client"; -import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; +import { type ISyncStateData, type SyncState } from "matrix-js-sdk/src/sync"; import { logger } from "matrix-js-sdk/src/logger"; import { secureRandomBase64Url } from "matrix-js-sdk/src/randomstring"; @@ -24,7 +24,10 @@ import IndexedDBWorker from "../IndexedDBWorker?worker"; import { generateUrlSearchParams, getUrlParams } from "../UrlParams"; import { Config } from "../config/Config"; import { E2eeType } from "../e2ee/e2eeType"; -import { EncryptionSystem, saveKeyForRoom } from "../e2ee/sharedKeyManagement"; +import { + type EncryptionSystem, + saveKeyForRoom, +} from "../e2ee/sharedKeyManagement"; export const fallbackICEServerAllowed = import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true"; @@ -333,15 +336,3 @@ export function getRelativeRoomUrl( : ""; return `/room/#${roomPart}?${generateUrlSearchParams(roomId, encryptionSystem, viaServers).toString()}`; } - -export function getAvatarUrl( - client: MatrixClient, - mxcUrl: string, - avatarSize = 96, -): string { - const width = Math.floor(avatarSize * window.devicePixelRatio); - const height = Math.floor(avatarSize * window.devicePixelRatio); - // scale is more suitable for larger sizes - const resizeMethod = avatarSize <= 96 ? "crop" : "scale"; - return mxcUrl && client.mxcUrlToHttp(mxcUrl, width, height, resizeMethod)!; -} diff --git a/src/utils/observable.ts b/src/utils/observable.ts index dc804941..a54c0293 100644 --- a/src/utils/observable.ts +++ b/src/utils/observable.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { Observable, defer, finalize, scan, startWith, tap } from "rxjs"; +import { type Observable, defer, finalize, scan, startWith, tap } from "rxjs"; const nothing = Symbol("nothing"); diff --git a/src/utils/spa.ts b/src/utils/spa.ts index 37835259..ab3dbea5 100644 --- a/src/utils/spa.ts +++ b/src/utils/spa.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ICreateClientOpts } from "matrix-js-sdk/src/client"; +import { type ICreateClientOpts } from "matrix-js-sdk/src/client"; import { MatrixError } from "matrix-js-sdk/src/http-api"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/utils/test.ts b/src/utils/test.ts index 5988dd6f..1cd21f01 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -4,16 +4,29 @@ Copyright 2023, 2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { map, Observable, of, SchedulerLike } from "rxjs"; -import { RunHelpers, TestScheduler } from "rxjs/testing"; -import { expect, vi } from "vitest"; -import { RoomMember, Room as MatrixRoom } from "matrix-js-sdk/src/matrix"; +import { map, type Observable, of, type SchedulerLike } from "rxjs"; +import { type RunHelpers, TestScheduler } from "rxjs/testing"; +import { expect, vi, vitest } from "vitest"; import { - LocalParticipant, - LocalTrackPublication, - RemoteParticipant, - RemoteTrackPublication, - Room as LivekitRoom, + type RoomMember, + type Room as MatrixRoom, + MatrixEvent, + type Room, + TypedEventEmitter, +} from "matrix-js-sdk/src/matrix"; +import { + CallMembership, + type Focus, + MatrixRTCSessionEvent, + type MatrixRTCSessionEventHandlerMap, + type SessionMembershipData, +} from "matrix-js-sdk/src/matrixrtc"; +import { + type LocalParticipant, + type LocalTrackPublication, + type RemoteParticipant, + type RemoteTrackPublication, + type Room as LivekitRoom, } from "livekit-client"; import { @@ -21,6 +34,11 @@ import { RemoteUserMediaViewModel, } from "../state/MediaViewModel"; import { E2eeType } from "../e2ee/e2eeType"; +import { + DEFAULT_CONFIG, + type ResolvedConfigOptions, +} from "../config/ConfigOptions"; +import { Config } from "../config/Config"; export function withFakeTimers(continuation: () => void): void { vi.useFakeTimers(); @@ -96,11 +114,40 @@ function mockEmitter(): EmitterMock { }; } +export function mockRtcMembership( + user: string | RoomMember, + deviceId: string, + callId = "", + fociPreferred: Focus[] = [], + focusActive: Focus = { type: "oldest_membership" }, + membership: Partial = {}, +): CallMembership { + const data: SessionMembershipData = { + application: "m.call", + call_id: callId, + device_id: deviceId, + foci_preferred: fociPreferred, + focus_active: focusActive, + ...membership, + }; + const event = new MatrixEvent({ + sender: typeof user === "string" ? user : user.userId, + }); + return new CallMembership(event, data); +} + // Maybe it'd be good to move this to matrix-js-sdk? Our testing needs are // rather simple, but if one util to mock a member is good enough for us, maybe // it's useful for matrix-js-sdk consumers in general. -export function mockMatrixRoomMember(member: Partial): RoomMember { - return { ...mockEmitter(), ...member } as RoomMember; +export function mockMatrixRoomMember( + rtcMembership: CallMembership, + member: Partial = {}, +): RoomMember { + return { + ...mockEmitter(), + userId: rtcMembership.sender, + ...member, + } as RoomMember; } export function mockMatrixRoom(room: Partial): MatrixRoom { @@ -143,14 +190,15 @@ export function mockLocalParticipant( } export async function withLocalMedia( - member: Partial, + localRtcMember: CallMembership, + roomMember: Partial, continuation: (vm: LocalUserMediaViewModel) => void | Promise, ): Promise { const localParticipant = mockLocalParticipant({}); const vm = new LocalUserMediaViewModel( "local", - mockMatrixRoomMember(member), - localParticipant, + mockMatrixRoomMember(localRtcMember, roomMember), + of(localParticipant), { kind: E2eeType.PER_PARTICIPANT, }, @@ -177,15 +225,16 @@ export function mockRemoteParticipant( } export async function withRemoteMedia( - member: Partial, + localRtcMember: CallMembership, + roomMember: Partial, participant: Partial, continuation: (vm: RemoteUserMediaViewModel) => void | Promise, ): Promise { const remoteParticipant = mockRemoteParticipant(participant); const vm = new RemoteUserMediaViewModel( "remote", - mockMatrixRoomMember(member), - remoteParticipant, + mockMatrixRoomMember(localRtcMember, roomMember), + of(remoteParticipant), { kind: E2eeType.PER_PARTICIPANT, }, @@ -197,3 +246,47 @@ export async function withRemoteMedia( vm.destroy(); } } + +export function mockConfig(config: Partial = {}): void { + vi.spyOn(Config, "get").mockReturnValue({ + ...DEFAULT_CONFIG, + ...config, + }); +} + +export class MockRTCSession extends TypedEventEmitter< + MatrixRTCSessionEvent, + MatrixRTCSessionEventHandlerMap +> { + public readonly statistics = { + counters: {}, + }; + + public leaveRoomSession = vitest.fn().mockResolvedValue(undefined); + + public constructor( + public readonly room: Room, + private localMembership: CallMembership, + public memberships: CallMembership[] = [], + ) { + super(); + } + + public isJoined(): true { + return true; + } + + public withMemberships( + rtcMembers: Observable[]>, + ): MockRTCSession { + rtcMembers.subscribe((m) => { + const old = this.memberships; + // always prepend the local participant + const updated = [this.localMembership, ...(m as CallMembership[])]; + this.memberships = updated; + this.emit(MatrixRTCSessionEvent.MembershipsChanged, old, updated); + }); + + return this; + } +} diff --git a/src/utils/testReactions.tsx b/src/utils/testReactions.tsx index 84ff217b..6fad030c 100644 --- a/src/utils/testReactions.tsx +++ b/src/utils/testReactions.tsx @@ -5,34 +5,34 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { PropsWithChildren, ReactNode } from "react"; +import { type PropsWithChildren, type ReactNode } from "react"; import { randomUUID } from "crypto"; import EventEmitter from "events"; -import { MatrixClient } from "matrix-js-sdk/src/client"; +import { type MatrixClient } from "matrix-js-sdk/src/client"; import { EventType, RoomEvent, RelationType } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, EventTimeline, EventTimelineSet, - Room, + type Room, } from "matrix-js-sdk/src/matrix"; import { - MatrixRTCSession, + type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/src/matrixrtc"; import { ReactionsProvider } from "../useReactions"; import { - ECallReactionEventContent, + type ECallReactionEventContent, ElementCallReactionEventType, - ReactionOption, + type ReactionOption, } from "../reactions"; export const TestReactionsWrapper = ({ rtcSession, children, }: PropsWithChildren<{ - rtcSession: MockRTCSession; + rtcSession: MockRTCSession | MatrixRTCSession; }>): ReactNode => { return ( @@ -203,4 +203,12 @@ export class MockRoom extends EventEmitter { }); return evt.getId()!; } + + public getMember(): void { + return; + } + + public testGetAsMatrixRoom(): Room { + return this as unknown as Room; + } } diff --git a/src/vitest.setup.ts b/src/vitest.setup.ts index 421ec663..46b370a9 100644 --- a/src/vitest.setup.ts +++ b/src/vitest.setup.ts @@ -15,21 +15,22 @@ import { afterEach } from "vitest"; import { cleanup } from "@testing-library/react"; import "vitest-axe/extend-expect"; import { logger } from "matrix-js-sdk/src/logger"; +import "@testing-library/jest-dom/vitest"; -import EN_GB from "../locales/en-GB/app.json"; +import EN from "../locales/en/app.json"; import { Config } from "./config/Config"; // Bare-minimum i18n config i18n .use(initReactI18next) .init({ - lng: "en-GB", - fallbackLng: "en-GB", - supportedLngs: ["en-GB"], + lng: "en", + fallbackLng: "en", + supportedLngs: ["en"], // We embed the translations, so that it never needs to fetch resources: { - "en-GB": { - app: EN_GB, + en: { + app: EN, }, }, interpolation: { diff --git a/vite.config.js b/vite.config.js index b8072577..1feb7d66 100644 --- a/vite.config.js +++ b/vite.config.js @@ -82,6 +82,10 @@ export default defineConfig(({ mode }) => { // Default naming fallback return "assets/[name]-[hash][extname]"; }, + manualChunks: { + // we should be able to remove this one https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/167 lands + "matrix-sdk-crypto-wasm": ["@matrix-org/matrix-sdk-crypto-wasm"], + }, }, }, }, diff --git a/yarn.lock b/yarn.lock index 7c883a6c..d607f394 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,11 @@ resolved "https://registry.yarnpkg.com/@actions/io/-/io-1.1.3.tgz#4cdb6254da7962b07473ff5c335f3da485d94d71" integrity sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q== +"@adobe/css-tools@^4.4.0": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.1.tgz#2447a230bfe072c1659e6815129c03cf170710e3" + integrity sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ== + "@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -48,7 +53,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -71,11 +76,11 @@ integrity sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA== "@babel/compat-data@^7.25.9": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" - integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" + integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== -"@babel/core@^7.16.5", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.25.2": +"@babel/core@^7.16.5", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== @@ -96,13 +101,13 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.25.9", "@babel/generator@^7.26.0": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" - integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== +"@babel/generator@^7.25.9", "@babel/generator@^7.26.0", "@babel/generator@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== dependencies: - "@babel/parser" "^7.26.2" - "@babel/types" "^7.26.0" + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" @@ -279,20 +284,20 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.20.7": - version "7.26.1" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.1.tgz#44e02499960df2cdce2c456372a3e8e0c3c5c975" - integrity sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw== - dependencies: - "@babel/types" "^7.26.0" - -"@babel/parser@^7.10.3", "@babel/parser@^7.25.4", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2": +"@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.20.7": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== dependencies: "@babel/types" "^7.26.0" +"@babel/parser@^7.25.4", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== + dependencies: + "@babel/types" "^7.26.3" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" @@ -686,14 +691,14 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.25.9" -"@babel/plugin-transform-react-jsx-self@^7.24.7": +"@babel/plugin-transform-react-jsx-self@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz#c0b6cae9c1b73967f7f9eb2fca9536ba2fad2858" integrity sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg== dependencies: "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-react-jsx-source@^7.24.7": +"@babel/plugin-transform-react-jsx-source@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz#4c6b8daa520b5f155b5fb55547d7c9fa91417503" integrity sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg== @@ -905,9 +910,9 @@ esutils "^2.0.2" "@babel/preset-react@^7.22.15": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.25.9.tgz#5f473035dc2094bcfdbc7392d0766bd42dce173e" - integrity sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.26.3.tgz#7c5e028d623b4683c1f83a0bd4713b9100560caa" + integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== dependencies: "@babel/helper-plugin-utils" "^7.25.9" "@babel/helper-validator-option" "^7.25.9" @@ -934,10 +939,10 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.12.5": - version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" - integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== +"@babel/runtime@^7.12.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.8.4": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== dependencies: regenerator-runtime "^0.14.0" @@ -948,13 +953,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.8.4": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" - integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== - dependencies: - regenerator-runtime "^0.14.0" - "@babel/template@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" @@ -964,7 +962,7 @@ "@babel/parser" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/traverse@^7.10.3", "@babel/traverse@^7.25.9": +"@babel/traverse@^7.10.3": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== @@ -977,7 +975,20 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.10.3", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.25.4", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.4.4": +"@babel/traverse@^7.25.9": + version "7.26.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.4.tgz#ac3a2a84b908dde6d463c3bfa2c5fdc1653574bd" + integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.3" + "@babel/parser" "^7.26.3" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.3" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.10.3", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.4.4": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== @@ -985,6 +996,14 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" +"@babel/types@^7.25.4", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -995,10 +1014,10 @@ resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-1.10.0.tgz#1a67ac889c2d464a3492b3e54c38f80517963b16" integrity sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag== -"@codecov/bundler-plugin-core@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.4.0.tgz#6035d8fe2a321b125c883ab77b9e6c36c9c08abd" - integrity sha512-/Rglx52KLdyqoZBW3DH2E/31c9/zWWZ4efTf+qxV0FSLb7oJ9/JZT3IBKL7f6fbVujR8PDMLIoG4Q0pmVY7LzA== +"@codecov/bundler-plugin-core@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.6.0.tgz#59da9dc464752ac4ce6f1fa142261aa42f6a8092" + integrity sha512-x2M5P1NUk5lNW5slKY3jSb6Hpuie7bKaolDyZ7oWBHvBgtAJOeU7VrutdVhaiYoiQonM65JI2UAIWtw6mup/Yw== dependencies: "@actions/core" "^1.10.1" "@actions/github" "^6.0.0" @@ -1008,11 +1027,11 @@ zod "^3.22.4" "@codecov/vite-plugin@^1.3.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@codecov/vite-plugin/-/vite-plugin-1.4.0.tgz#01e4ec2a0b7c144b054ba5876bc5ab5d577a3112" - integrity sha512-4pf9rZJLR/eqeoY0QY1pgAJs/tdg1os9xjgBBWuhQ/iLYseQZ3q1qn3G8QGuaSUS7XB/Sje3BQ5qGBM1hzE8Sw== + version "1.6.0" + resolved "https://registry.yarnpkg.com/@codecov/vite-plugin/-/vite-plugin-1.6.0.tgz#5433600f1df8528d4ce693cc1ae9297b07b197d5" + integrity sha512-QwgFfF0FJMXovE/ZX33GqkBjkwUwzjPkWepwJizXiQD9emFS7iW82q1vPV9goiakJAvsCVm7Au9e7QnMBGJgvw== dependencies: - "@codecov/bundler-plugin-core" "^1.4.0" + "@codecov/bundler-plugin-core" "^1.6.0" unplugin "^1.10.1" "@csstools/cascade-layer-name-parser@^2.0.4": @@ -1347,6 +1366,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== +"@esbuild/aix-ppc64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c" + integrity sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw== + "@esbuild/android-arm64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" @@ -1357,6 +1381,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== +"@esbuild/android-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz#1add7e0af67acefd556e407f8497e81fddad79c0" + integrity sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w== + "@esbuild/android-arm@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" @@ -1367,6 +1396,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== +"@esbuild/android-arm@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.0.tgz#ab7263045fa8e090833a8e3c393b60d59a789810" + integrity sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew== + "@esbuild/android-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" @@ -1377,6 +1411,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== +"@esbuild/android-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.0.tgz#e8f8b196cfdfdd5aeaebbdb0110983460440e705" + integrity sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ== + "@esbuild/darwin-arm64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" @@ -1387,6 +1426,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== +"@esbuild/darwin-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz#2d0d9414f2acbffd2d86e98253914fca603a53dd" + integrity sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw== + "@esbuild/darwin-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" @@ -1397,6 +1441,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== +"@esbuild/darwin-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz#33087aab31a1eb64c89daf3d2cf8ce1775656107" + integrity sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA== + "@esbuild/freebsd-arm64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" @@ -1407,6 +1456,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== +"@esbuild/freebsd-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz#bb76e5ea9e97fa3c753472f19421075d3a33e8a7" + integrity sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA== + "@esbuild/freebsd-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" @@ -1417,6 +1471,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== +"@esbuild/freebsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz#e0e2ce9249fdf6ee29e5dc3d420c7007fa579b93" + integrity sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ== + "@esbuild/linux-arm64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" @@ -1427,6 +1486,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== +"@esbuild/linux-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz#d1b2aa58085f73ecf45533c07c82d81235388e75" + integrity sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g== + "@esbuild/linux-arm@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" @@ -1437,6 +1501,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== +"@esbuild/linux-arm@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz#8e4915df8ea3e12b690a057e77a47b1d5935ef6d" + integrity sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw== + "@esbuild/linux-ia32@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" @@ -1447,6 +1516,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== +"@esbuild/linux-ia32@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz#8200b1110666c39ab316572324b7af63d82013fb" + integrity sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA== + "@esbuild/linux-loong64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" @@ -1457,6 +1531,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== +"@esbuild/linux-loong64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz#6ff0c99cf647504df321d0640f0d32e557da745c" + integrity sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g== + "@esbuild/linux-mips64el@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" @@ -1467,6 +1546,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== +"@esbuild/linux-mips64el@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz#3f720ccd4d59bfeb4c2ce276a46b77ad380fa1f3" + integrity sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA== + "@esbuild/linux-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" @@ -1477,6 +1561,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== +"@esbuild/linux-ppc64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz#9d6b188b15c25afd2e213474bf5f31e42e3aa09e" + integrity sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ== + "@esbuild/linux-riscv64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" @@ -1487,6 +1576,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== +"@esbuild/linux-riscv64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz#f989fdc9752dfda286c9cd87c46248e4dfecbc25" + integrity sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw== + "@esbuild/linux-s390x@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" @@ -1497,6 +1591,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== +"@esbuild/linux-s390x@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz#29ebf87e4132ea659c1489fce63cd8509d1c7319" + integrity sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g== + "@esbuild/linux-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" @@ -1507,6 +1606,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== +"@esbuild/linux-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz#4af48c5c0479569b1f359ffbce22d15f261c0cef" + integrity sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA== + "@esbuild/netbsd-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" @@ -1517,11 +1621,21 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== +"@esbuild/netbsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz#1ae73d23cc044a0ebd4f198334416fb26c31366c" + integrity sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg== + "@esbuild/openbsd-arm64@0.23.0": version "0.23.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== +"@esbuild/openbsd-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz#5d904a4f5158c89859fd902c427f96d6a9e632e2" + integrity sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg== + "@esbuild/openbsd-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" @@ -1532,6 +1646,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== +"@esbuild/openbsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz#4c8aa88c49187c601bae2971e71c6dc5e0ad1cdf" + integrity sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q== + "@esbuild/sunos-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" @@ -1542,6 +1661,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== +"@esbuild/sunos-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz#8ddc35a0ea38575fa44eda30a5ee01ae2fa54dd4" + integrity sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA== + "@esbuild/win32-arm64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" @@ -1552,6 +1676,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== +"@esbuild/win32-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz#6e79c8543f282c4539db684a207ae0e174a9007b" + integrity sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA== + "@esbuild/win32-ia32@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" @@ -1562,6 +1691,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== +"@esbuild/win32-ia32@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz#057af345da256b7192d18b676a02e95d0fa39103" + integrity sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw== + "@esbuild/win32-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" @@ -1572,6 +1706,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== +"@esbuild/win32-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244" + integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA== + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1675,45 +1814,46 @@ resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.1.0.tgz#ab629b2c662457022d2d6a29854b8dc8ba538c47" integrity sha512-zKZR3kf1G0noIes1frLfOHP5EXVVm0M7sV/l9f/AaYf+M/DId35FO4LkigWjqWYjTJZGgplhdv4cB+ssvCqr5A== -"@formatjs/ecma402-abstract@2.2.4": - version "2.2.4" - resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz#355e42d375678229d46dc8ad7a7139520dd03e7b" - integrity sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg== +"@formatjs/ecma402-abstract@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.1.tgz#cdeb3ffe1aeea9c4284b85b7e37e8e8615314c39" + integrity sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw== dependencies: - "@formatjs/fast-memoize" "2.2.3" - "@formatjs/intl-localematcher" "0.5.8" + "@formatjs/fast-memoize" "2.2.5" + "@formatjs/intl-localematcher" "0.5.9" + decimal.js "10" tslib "2" -"@formatjs/fast-memoize@2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz#74e64109279d5244f9fc281f3ae90c407cece823" - integrity sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA== +"@formatjs/fast-memoize@2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.5.tgz#54a4a1793d773b72c372d3dcab3595149aee7880" + integrity sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g== dependencies: tslib "2" -"@formatjs/intl-durationformat@^0.6.1": - version "0.6.4" - resolved "https://registry.yarnpkg.com/@formatjs/intl-durationformat/-/intl-durationformat-0.6.4.tgz#ac6b5ab006cf2b57500cce05dd1d201352500471" - integrity sha512-kpYLechF9ZvECzzMsvikBl48GkbCEAbZJN4kG/4x0FTVZkBuOWrBlj6DghCn7YsW3Bgsr0n9E0RYO373Kg3m+Q== +"@formatjs/intl-durationformat@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@formatjs/intl-durationformat/-/intl-durationformat-0.7.1.tgz#d83e6f4bf188cafac50a2a911241084bafe89524" + integrity sha512-tM/sscHRcVMVAn0qMJlmq5mf3MaqA0jSz73NT4SYBHZuZqfU0EKWjJCwZBYeNRfvO6y20Yo0RzGxom0KvSVUlA== dependencies: - "@formatjs/ecma402-abstract" "2.2.4" - "@formatjs/intl-localematcher" "0.5.8" + "@formatjs/ecma402-abstract" "2.3.1" + "@formatjs/intl-localematcher" "0.5.9" tslib "2" -"@formatjs/intl-localematcher@0.5.8": - version "0.5.8" - resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz#b11bbd04bd3551f7cadcb1ef1e231822d0e3c97e" - integrity sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg== +"@formatjs/intl-localematcher@0.5.9": + version "0.5.9" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.9.tgz#43c6ee22be85b83340bcb09bdfed53657a2720db" + integrity sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA== dependencies: tslib "2" "@formatjs/intl-segmenter@^11.7.3": - version "11.7.4" - resolved "https://registry.yarnpkg.com/@formatjs/intl-segmenter/-/intl-segmenter-11.7.4.tgz#f99d87ee3f98515069285438a4913681fc243252" - integrity sha512-pyHgFO86/CReKl20oK9jgaTMzSaG/nIMteMW8YuwUcS22EoMI1qbGTZ65oQ38KMT05SiHiMee2CP3WZvCi8YSQ== + version "11.7.7" + resolved "https://registry.yarnpkg.com/@formatjs/intl-segmenter/-/intl-segmenter-11.7.7.tgz#8a5aaa316e11ca2d31b99222e6fcf1ab539b085e" + integrity sha512-610J5xz5DxtEpa16zNR89CrvA9qWHxQFkUB3FKiGao0Nwn7i8cl+oyBhuH9SvtXF9j2LUOM9VMdVCMzJkVANNw== dependencies: - "@formatjs/ecma402-abstract" "2.2.4" - "@formatjs/intl-localematcher" "0.5.8" + "@formatjs/ecma402-abstract" "2.3.1" + "@formatjs/intl-localematcher" "0.5.9" tslib "2" "@gulpjs/to-absolute-glob@^4.0.0": @@ -1801,9 +1941,9 @@ rxjs "7.8.1" "@livekit/components-react@^2.0.0": - version "2.6.8" - resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-2.6.8.tgz#faa60410aef0f5d426afcc6f9b577686983c6b7b" - integrity sha512-G6P+mrOyBiAnHjbmBTG28CxA6AT7wXT6/5dqu7M7uZAlvOCDKhPjhOs65awDQvaFlTxd/JlND75fa9d+oSbvIA== + version "2.6.9" + resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-2.6.9.tgz#2ff4691dc2cae6ed4c4b2e586a255d00e494bf9c" + integrity sha512-j43i/Dm8dlI2jxv5wv0s+69QPVqVEjg0y2tyznfs/7RDcaIZsIIzNijPu1kLditerzvzQdRsOgFQ3UWONcTkGA== dependencies: "@livekit/components-core" "0.11.10" clsx "2.1.1" @@ -1814,17 +1954,17 @@ resolved "https://registry.yarnpkg.com/@livekit/mutex/-/mutex-1.0.0.tgz#9493102d92ff75dfb0445eccc46c7c7ac189d385" integrity sha512-aiUhoThBNF9UyGTxEURFzJLhhPLIVTnQiEVMjRhPnfHNKLfo2JY9xovHKIus7B78UD5hsP6DlgpmAsjrz4U0Iw== -"@livekit/protocol@1.24.0": - version "1.24.0" - resolved "https://registry.yarnpkg.com/@livekit/protocol/-/protocol-1.24.0.tgz#b23acab25c11027bf26c1b42f9b782682f2da585" - integrity sha512-9dCsqnkMn7lvbI4NGh18zhLDsrXyUcpS++TEFgEk5Xv1WM3R2kT3EzqgL1P/mr3jaabM6rJ8wZA/KJLuQNpF5w== +"@livekit/protocol@1.29.4": + version "1.29.4" + resolved "https://registry.yarnpkg.com/@livekit/protocol/-/protocol-1.29.4.tgz#346906d080bc8207a80570b45db91153a495e0dc" + integrity sha512-dsqxvABHilrMA0BU5m1w8cMWSVeDjV2ZUIUDClNQZju3c30DLMfEYDHU5nmXDfaaHjNIgoRbYR7upJMozG8JJg== dependencies: "@bufbuild/protobuf" "^1.10.0" -"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.0.0.tgz#293fe8fcb9bc4d577c5f6cf2cbffa151c6e11329" - integrity sha512-dz4dkYXj6BeOQuw52XQj8dMuhi85pSFhfFeFlNRAO7JdRPhE9CHBrfK8knkZV5Zux5vvf3Ub4E7myoLeJgZoEw== +"@matrix-org/matrix-sdk-crypto-wasm@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.0.0.tgz#c49a1a0d1e367d3c00a2144a4ab23caee0b1eec2" + integrity sha512-a7NUH8Kjc8hwzNCPpkOGXoceFqWJiWvA8OskXeDrKyODJuDz4yKrZ/nvgaVRfQe45Ab5UC1ZXYqaME+ChlJuqg== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -1938,16 +2078,16 @@ "@octokit/openapi-types" "^20.0.0" "@octokit/types@^13.0.0", "@octokit/types@^13.1.0": - version "13.6.1" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.1.tgz#432fc6c0aaae54318e5b2d3e15c22ac97fc9b15f" - integrity sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g== + version "13.6.2" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd" + integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA== dependencies: "@octokit/openapi-types" "^22.2.0" -"@opentelemetry/api-logs@0.54.2": - version "0.54.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.54.2.tgz#bb8aa11cdc69b327b58d7e10cc2bc26bf540421f" - integrity sha512-4MTVwwmLgUh5QrJnZpYo6YRO5IBLAggf2h8gWDblwRagDStY13aEvt7gGk3jewrMaPlHiF83fENhIx0HO97/cQ== +"@opentelemetry/api-logs@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.56.0.tgz#68f8c51ca905c260b610c8a3c67d3f9fa3d59a45" + integrity sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g== dependencies: "@opentelemetry/api" "^1.3.0" @@ -1956,92 +2096,92 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== -"@opentelemetry/core@1.27.0", "@opentelemetry/core@^1.25.1": - version "1.27.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.27.0.tgz#9f1701a654ab01abcebb12931b418f3393b94b75" - integrity sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg== +"@opentelemetry/core@1.29.0", "@opentelemetry/core@^1.25.1": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.29.0.tgz#a9397dfd9a8b37b2435b5e44be16d39ec1c82bd9" + integrity sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA== dependencies: - "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/semantic-conventions" "1.28.0" -"@opentelemetry/exporter-trace-otlp-http@^0.54.0": - version "0.54.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.54.2.tgz#e6fab84405b95ece2977a80e4cec907e568ef0f3" - integrity sha512-BgWKKyD/h2zpISdmYHN/sapwTjvt1P4p5yx4xeBV8XAEqh4OQUhOtSGFG80+nPQ1F8of3mKOT1DDoDbJp1u25w== +"@opentelemetry/exporter-trace-otlp-http@^0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.56.0.tgz#184bd208d68bd19c3382a9a22737200b34f7edb9" + integrity sha512-vqVuJvcwameA0r0cNrRzrZqPLB0otS+95g0XkZdiKOXUo81wYdY6r4kyrwz4nSChqTBEFm0lqi/H2OWGboOa6g== dependencies: - "@opentelemetry/core" "1.27.0" - "@opentelemetry/otlp-exporter-base" "0.54.2" - "@opentelemetry/otlp-transformer" "0.54.2" - "@opentelemetry/resources" "1.27.0" - "@opentelemetry/sdk-trace-base" "1.27.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/otlp-exporter-base" "0.56.0" + "@opentelemetry/otlp-transformer" "0.56.0" + "@opentelemetry/resources" "1.29.0" + "@opentelemetry/sdk-trace-base" "1.29.0" -"@opentelemetry/otlp-exporter-base@0.54.2": - version "0.54.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.54.2.tgz#fb2361665baec9e9600c5408747fc03124889f0a" - integrity sha512-NrNyxu6R/bGAwanhz1HI0aJWKR6xUED4TjCH4iWMlAfyRukGbI9Kt/Akd2sYLwRKNhfS+sKetKGCUQPMDyYYMA== +"@opentelemetry/otlp-exporter-base@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.56.0.tgz#3461fd403fbd3d366df46536a5a7dd7c7f499536" + integrity sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw== dependencies: - "@opentelemetry/core" "1.27.0" - "@opentelemetry/otlp-transformer" "0.54.2" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/otlp-transformer" "0.56.0" -"@opentelemetry/otlp-transformer@0.54.2": - version "0.54.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.54.2.tgz#5952072cf37a7d5da0ac5491426126459c13c839" - integrity sha512-2tIjahJlMRRUz0A2SeE+qBkeBXBFkSjR0wqJ08kuOqaL8HNGan5iZf+A8cfrfmZzPUuMKCyY9I+okzFuFs6gKQ== +"@opentelemetry/otlp-transformer@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.56.0.tgz#d2bae377ff2cabc0366d002ab993fcb8ea7d2700" + integrity sha512-kVkH/W2W7EpgWWpyU5VnnjIdSD7Y7FljQYObAQSKdRcejiwMj2glypZtUdfq1LTJcv4ht0jyTrw1D3CCxssNtQ== dependencies: - "@opentelemetry/api-logs" "0.54.2" - "@opentelemetry/core" "1.27.0" - "@opentelemetry/resources" "1.27.0" - "@opentelemetry/sdk-logs" "0.54.2" - "@opentelemetry/sdk-metrics" "1.27.0" - "@opentelemetry/sdk-trace-base" "1.27.0" + "@opentelemetry/api-logs" "0.56.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/resources" "1.29.0" + "@opentelemetry/sdk-logs" "0.56.0" + "@opentelemetry/sdk-metrics" "1.29.0" + "@opentelemetry/sdk-trace-base" "1.29.0" protobufjs "^7.3.0" -"@opentelemetry/resources@1.27.0", "@opentelemetry/resources@^1.25.1": - version "1.27.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.27.0.tgz#1f91c270eb95be32f3511e9e6624c1c0f993c4ac" - integrity sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ== +"@opentelemetry/resources@1.29.0", "@opentelemetry/resources@^1.25.1": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.29.0.tgz#d170f39b2ac93d61b53d13dfcd96795181bdc372" + integrity sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ== dependencies: - "@opentelemetry/core" "1.27.0" - "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/semantic-conventions" "1.28.0" -"@opentelemetry/sdk-logs@0.54.2": - version "0.54.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-logs/-/sdk-logs-0.54.2.tgz#07cc61135b5acb09affa8cd290966027ee8c886a" - integrity sha512-yIbYqDLS/AtBbPjCjh6eSToGNRMqW2VR8RrKEy+G+J7dFG7pKoptTH5T+XlKPleP9NY8JZYIpgJBlI+Osi0rFw== +"@opentelemetry/sdk-logs@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-logs/-/sdk-logs-0.56.0.tgz#2ce3416111d1524305f4ec92dccf9e9f9e9626cf" + integrity sha512-OS0WPBJF++R/cSl+terUjQH5PebloidB1Jbbecgg2rnCmQbTST9xsRes23bLfDQVRvmegmHqDh884h0aRdJyLw== dependencies: - "@opentelemetry/api-logs" "0.54.2" - "@opentelemetry/core" "1.27.0" - "@opentelemetry/resources" "1.27.0" + "@opentelemetry/api-logs" "0.56.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/resources" "1.29.0" -"@opentelemetry/sdk-metrics@1.27.0": - version "1.27.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.27.0.tgz#fb4f55017dc95a95ee00260262952b18e3e7c25c" - integrity sha512-JzWgzlutoXCydhHWIbLg+r76m+m3ncqvkCcsswXAQ4gqKS+LOHKhq+t6fx1zNytvLuaOUBur7EvWxECc4jPQKg== +"@opentelemetry/sdk-metrics@1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.29.0.tgz#26b9891e47715c0caaaa4d4e8b536685e1937a06" + integrity sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw== dependencies: - "@opentelemetry/core" "1.27.0" - "@opentelemetry/resources" "1.27.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/resources" "1.29.0" -"@opentelemetry/sdk-trace-base@1.27.0", "@opentelemetry/sdk-trace-base@^1.25.1": - version "1.27.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.27.0.tgz#2276e4cd0d701a8faba77382b2938853a0907b54" - integrity sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ== +"@opentelemetry/sdk-trace-base@1.29.0", "@opentelemetry/sdk-trace-base@^1.25.1": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.29.0.tgz#f48d95dae0e58e601d0596bd2e482122d2688fb8" + integrity sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ== dependencies: - "@opentelemetry/core" "1.27.0" - "@opentelemetry/resources" "1.27.0" - "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/resources" "1.29.0" + "@opentelemetry/semantic-conventions" "1.28.0" "@opentelemetry/sdk-trace-web@^1.9.1": - version "1.27.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.27.0.tgz#cdc9b43aab44b12741e408fb70b2bc0e941f0c7c" - integrity sha512-ORZfG8Sm5IkJeI+P8MyW8v4m5OcmjEtD7VsjBghv5sDKH3f5p2mQpEEoJWlCr5GiW50Y1MaI2R4uFGIsxmDE9A== + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.29.0.tgz#0d0321b511011a0174662bec821f046a55de51e8" + integrity sha512-PQVtJ76dsZ7HYBSlgZGIuxFtnKXxNbyHzMnRUxww7V2/6V/qtQN+cvNkqwPVffrUfbvClOnejo08NezAE1y+6g== dependencies: - "@opentelemetry/core" "1.27.0" - "@opentelemetry/sdk-trace-base" "1.27.0" - "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/sdk-trace-base" "1.29.0" + "@opentelemetry/semantic-conventions" "1.28.0" -"@opentelemetry/semantic-conventions@1.27.0", "@opentelemetry/semantic-conventions@^1.25.1": - version "1.27.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz#1a857dcc95a5ab30122e04417148211e6f945e6c" - integrity sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg== +"@opentelemetry/semantic-conventions@1.28.0", "@opentelemetry/semantic-conventions@^1.25.1": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6" + integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA== "@parcel/watcher-android-arm64@2.5.0": version "2.5.0" @@ -2564,205 +2704,285 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz#3e7eda4c0c1de6d2415343002d742ff95e38dca7" integrity sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA== +"@rollup/rollup-android-arm-eabi@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz#462e7ecdd60968bc9eb95a20d185e74f8243ec1b" + integrity sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ== + "@rollup/rollup-android-arm64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz#04f679231acf7284f1f8a1f7250d0e0944865ba8" integrity sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg== +"@rollup/rollup-android-arm64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz#78a2b8a8a55f71a295eb860a654ae90a2b168f40" + integrity sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA== + "@rollup/rollup-darwin-arm64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz#ecea723041621747d0772af93b54752edf26467a" integrity sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg== +"@rollup/rollup-darwin-arm64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz#5b783af714f434f1e66e3cdfa3817e0b99216d84" + integrity sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q== + "@rollup/rollup-darwin-x64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz#28e6e0687092f31e20982fc104779d48c643fc21" integrity sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA== +"@rollup/rollup-darwin-x64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz#f72484e842521a5261978034e18e20f778a2850d" + integrity sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w== + "@rollup/rollup-freebsd-arm64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz#99e9173b8aef3d1ef086983da70413988206e530" integrity sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g== +"@rollup/rollup-freebsd-arm64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz#3c919dff72b2fe344811a609c674a8347b033f62" + integrity sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ== + "@rollup/rollup-freebsd-x64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz#f3a1ef941f8d3c6b2b036484c69a7b2d3d9ebbd7" integrity sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw== +"@rollup/rollup-freebsd-x64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz#b62a3a8365b363b3fdfa6da11a9188b6ab4dca7c" + integrity sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA== + "@rollup/rollup-linux-arm-gnueabihf@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz#9ba6adcc33f26f2a0c6ee658f0bbda4de8da2f75" integrity sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA== +"@rollup/rollup-linux-arm-gnueabihf@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz#0d02cc55bd229bd8ca5c54f65f916ba5e0591c94" + integrity sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w== + "@rollup/rollup-linux-arm-musleabihf@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz#62f2426fa9016ec884f4fa779d7b62d5ba02a41a" integrity sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ== +"@rollup/rollup-linux-arm-musleabihf@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz#c51d379263201e88a60e92bd8e90878f0c044425" + integrity sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg== + "@rollup/rollup-linux-arm64-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz#f98ec111a231d35e0c6d3404e3d80f67f9d5b9f8" integrity sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A== +"@rollup/rollup-linux-arm64-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz#93ce2addc337b5cfa52b84f8e730d2e36eb4339b" + integrity sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg== + "@rollup/rollup-linux-arm64-musl@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz#4b36ffb8359f959f2c29afd187603c53368b6723" integrity sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw== +"@rollup/rollup-linux-arm64-musl@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz#730af6ddc091a5ba5baac28a3510691725dc808b" + integrity sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw== + "@rollup/rollup-linux-powerpc64le-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz#52f4b39e6783505d168a745b79d86474fde71680" integrity sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA== +"@rollup/rollup-linux-powerpc64le-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz#b5565aac20b4de60ca1e557f525e76478b5436af" + integrity sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ== + "@rollup/rollup-linux-riscv64-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz#49195be7e6a7d68d482b12461e2ea914e31ff977" integrity sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA== +"@rollup/rollup-linux-riscv64-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz#d488290bf9338bad4ae9409c4aa8a1728835a20b" + integrity sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g== + "@rollup/rollup-linux-s390x-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz#4b8d50a205eac7b46cdcb9c50d4a6ae5994c02e0" integrity sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ== +"@rollup/rollup-linux-s390x-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz#eb2e3f3a06acf448115045c11a5a96868c95a556" + integrity sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw== + "@rollup/rollup-linux-x64-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz#dfcceebc5ccac7fc2db19471996026258c81b55f" integrity sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig== +"@rollup/rollup-linux-x64-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz#065952ef2aea7e837dc7e02aa500feeaff4fc507" + integrity sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw== + "@rollup/rollup-linux-x64-musl@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz#192f78bad8429711d63a31dc0a7d3312e2df850e" integrity sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ== +"@rollup/rollup-linux-x64-musl@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz#3435d484d05f5c4d1ffd54541b4facce2887103a" + integrity sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw== + "@rollup/rollup-win32-arm64-msvc@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz#f4ec076579634f780b4e5896ae7f59f3e38e0c60" integrity sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww== +"@rollup/rollup-win32-arm64-msvc@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz#69682a2a10d9fedc334f87583cfca83c39c08077" + integrity sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg== + "@rollup/rollup-win32-ia32-msvc@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz#5458eab1929827e4f805cefb90bd09ecf7eeed2b" integrity sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg== +"@rollup/rollup-win32-ia32-msvc@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz#b64470f9ac79abb386829c56750b9a4711be3332" + integrity sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A== + "@rollup/rollup-win32-x64-msvc@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz#93415e7e707e4b156d77c5950b983b58f4bc33f3" integrity sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg== +"@rollup/rollup-win32-x64-msvc@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz#cb313feef9ac6e3737067fdf34f42804ac65a6f2" + integrity sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ== + "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sentry-internal/browser-utils@8.38.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.38.0.tgz#d7f6d398778906efb0c017e63d3c59d3203dfa7d" - integrity sha512-5QMVcssrAcmjKT0NdFYcX0b0wwZovGAZ9L2GajErXtHkBenjI2sgR2+5J7n+QZGuk2SC1qhGmT1O9i3p3UEwew== +"@sentry-internal/browser-utils@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.43.0.tgz#b064908a537d1cc17d8ddaf0f4c5d712557cbf40" + integrity sha512-5WhJZ3SA5sZVDBwOsChDd5JCzYcwBX7sEqBqEcm3pFru6TUihEnFIJmDIbreIyrQMwUhs3dTxnfnidgjr5z1Ag== dependencies: - "@sentry/core" "8.38.0" - "@sentry/types" "8.38.0" - "@sentry/utils" "8.38.0" + "@sentry/core" "8.43.0" -"@sentry-internal/feedback@8.38.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.38.0.tgz#726661a01f7ff40b93c8ee05c985fd0436a1c033" - integrity sha512-AW5HCCAlc3T1jcSuNhbFVNO1CHyJ5g5tsGKEP4VKgu+D1Gg2kZ5S2eFatLBUP/BD5JYb1A7p6XPuzYp1XfMq0A== +"@sentry-internal/feedback@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.43.0.tgz#9477b999c9bca62335eb944a6f7246a96beb0111" + integrity sha512-rcGR2kzFu4vLXBQbI9eGJwjyToyjl36O2q/UKbiZBNJ5IFtDvKRLke6jIHq/YqiHPfFGpVtq5M/lYduDfA/eaQ== dependencies: - "@sentry/core" "8.38.0" - "@sentry/types" "8.38.0" - "@sentry/utils" "8.38.0" + "@sentry/core" "8.43.0" -"@sentry-internal/replay-canvas@8.38.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.38.0.tgz#26e9bc937dab73e1a26d57dc1015b7ff1f2d76c5" - integrity sha512-OxmlWzK9J8mRM+KxdSnQ5xuxq+p7TiBzTz70FT3HltxmeugvDkyp6803UcFqHOPHR35OYeVLOalym+FmvNn9kw== +"@sentry-internal/replay-canvas@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.43.0.tgz#f5672a08c9eb588afa0bf36f07b9f5c29b5c9920" + integrity sha512-rL8G7E1GtozH8VNalRrBQNjYDJ5ChWS/vpQI5hUG11PZfvQFXEVatLvT3uO2l0xIlHm4idTsHOSLTe/usxnogQ== dependencies: - "@sentry-internal/replay" "8.38.0" - "@sentry/core" "8.38.0" - "@sentry/types" "8.38.0" - "@sentry/utils" "8.38.0" + "@sentry-internal/replay" "8.43.0" + "@sentry/core" "8.43.0" -"@sentry-internal/replay@8.38.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.38.0.tgz#9a9b945a3c066f5610a363774e3c99420c3f4fce" - integrity sha512-mQPShKnIab7oKwkwrRxP/D8fZYHSkDY+cvqORzgi+wAwgnunytJQjz9g6Ww2lJu98rHEkr5SH4V4rs6PZYZmnQ== +"@sentry-internal/replay@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.43.0.tgz#4e2e3844f52b47b16bf816d21857921bbfe85d62" + integrity sha512-geV5/zejLfGGwWHjylzrb1w8NI3U37GMG9/53nmv13FmTXUDF5XF2lh41KXFVYwvp7Ha4bd1FRQ9IU9YtBWskw== dependencies: - "@sentry-internal/browser-utils" "8.38.0" - "@sentry/core" "8.38.0" - "@sentry/types" "8.38.0" - "@sentry/utils" "8.38.0" + "@sentry-internal/browser-utils" "8.43.0" + "@sentry/core" "8.43.0" -"@sentry/babel-plugin-component-annotate@2.22.6": - version "2.22.6" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.6.tgz#829d6caf2c95c1c46108336de4e1049e6521435e" - integrity sha512-V2g1Y1I5eSe7dtUVMBvAJr8BaLRr4CLrgNgtPaZyMT4Rnps82SrZ5zqmEkLXPumlXhLUWR6qzoMNN2u+RXVXfQ== +"@sentry/babel-plugin-component-annotate@2.22.7": + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.7.tgz#604c7e33d48528a13477e7af597c4d5fca51b8bd" + integrity sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ== -"@sentry/browser@8.38.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.38.0.tgz#c562accdc2bbe0b0074d98bfe7ff460e39ce3109" - integrity sha512-AZR+b0EteNZEGv6JSdBD22S9VhQ7nrljKsSnzxobBULf3BpwmhmCzTbDrqWszKDAIDYmL+yQJIR2glxbknneWQ== +"@sentry/browser@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.43.0.tgz#4eec67bc6fb278727304045b612ac392674cade6" + integrity sha512-LGvLLnfmR8+AEgFmd7Q7KHiOTiV0P1Lvio2ENDELhEqJOIiICauttibVmig+AW02qg4kMeywvleMsUYaZv2RVA== dependencies: - "@sentry-internal/browser-utils" "8.38.0" - "@sentry-internal/feedback" "8.38.0" - "@sentry-internal/replay" "8.38.0" - "@sentry-internal/replay-canvas" "8.38.0" - "@sentry/core" "8.38.0" - "@sentry/types" "8.38.0" - "@sentry/utils" "8.38.0" + "@sentry-internal/browser-utils" "8.43.0" + "@sentry-internal/feedback" "8.43.0" + "@sentry-internal/replay" "8.43.0" + "@sentry-internal/replay-canvas" "8.43.0" + "@sentry/core" "8.43.0" -"@sentry/bundler-plugin-core@2.22.6": - version "2.22.6" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.6.tgz#a1ea1fd43700a3ece9e7db016997e79a2782b87d" - integrity sha512-1esQdgSUCww9XAntO4pr7uAM5cfGhLsgTK9MEwAKNfvpMYJi9NUTYa3A7AZmdA8V6107Lo4OD7peIPrDRbaDCg== +"@sentry/bundler-plugin-core@2.22.7": + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.7.tgz#28204a224cd1fef58d157e5beeb2493947a9bc35" + integrity sha512-ouQh5sqcB8vsJ8yTTe0rf+iaUkwmeUlGNFi35IkCFUQlWJ22qS6OfvNjOqFI19e6eGUXks0c/2ieFC4+9wJ+1g== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "2.22.6" - "@sentry/cli" "^2.36.1" + "@sentry/babel-plugin-component-annotate" "2.22.7" + "@sentry/cli" "2.39.1" dotenv "^16.3.1" find-up "^5.0.0" glob "^9.3.2" magic-string "0.30.8" unplugin "1.0.1" -"@sentry/cli-darwin@2.38.1": - version "2.38.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.38.1.tgz#f6e48caaba2d9e813c8d44ce26084634d22b6b42" - integrity sha512-IHuxm072aSTAvwuHtLg065cF00Pxm2wprnrRr2lkyWp8nLOoO7DmumWZ4pjHvhB8yZXsAbM/PSxLRBoDIRDPzQ== +"@sentry/cli-darwin@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.39.1.tgz#75c338a53834b4cf72f57599f4c72ffb36cf0781" + integrity sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ== -"@sentry/cli-linux-arm64@2.38.1": - version "2.38.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.38.1.tgz#178b14747bf7070d4b17f2a3108ea0c45d02a2dc" - integrity sha512-3bj5DS4wDusL0YHwG5qeI+O19kz4N4KDDmnWqIew56MmSSAEM5B0qKk5Hivu1vRU5vPKFwVn8BVjLnKXu9idjg== +"@sentry/cli-linux-arm64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.39.1.tgz#27db44700c33fcb1e8966257020b43f8494373e6" + integrity sha512-5VbVJDatolDrWOgaffsEM7znjs0cR8bHt9Bq0mStM3tBolgAeSDHE89NgHggfZR+DJ2VWOy4vgCwkObrUD6NQw== -"@sentry/cli-linux-arm@2.38.1": - version "2.38.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.38.1.tgz#d0d64ec17692792769539d33393dcf66c45b3a35" - integrity sha512-xyf4f56O4/eeirol8t1tTQw0cwF34b3v69zn6wHtKfx2lW5IEBGO+agVNdOdosnCx6j3UadgdRXUJlSyM9kx/w== +"@sentry/cli-linux-arm@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.39.1.tgz#451683fa9a5a60b1359d104ec71334ed16f4b63c" + integrity sha512-DkENbxyRxUrfLnJLXTA4s5UL/GoctU5Cm4ER1eB7XN7p9WsamFJd/yf2KpltkjEyiTuplv0yAbdjl1KX3vKmEQ== -"@sentry/cli-linux-i686@2.38.1": - version "2.38.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.38.1.tgz#c6cc9ac8427fbc343e0cb4cdeee97cd5998e04fb" - integrity sha512-VygJO2oTc6GfiqqmPYNpO2bW1hzszuNyn37SSmeRuuhq1/kRwD+ZQj4OmXYEASjSLg+8mDPoWOurPjHEPKNtNw== +"@sentry/cli-linux-i686@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.39.1.tgz#9965a81f97a94e8b6d1d15589e43fee158e35201" + integrity sha512-pXWVoKXCRrY7N8vc9H7mETiV9ZCz+zSnX65JQCzZxgYrayQPJTc+NPRnZTdYdk5RlAupXaFicBI2GwOCRqVRkg== -"@sentry/cli-linux-x64@2.38.1": - version "2.38.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.38.1.tgz#5d53aeb00dc16a59f9ec20058375fb6ceace12c4" - integrity sha512-9SaPJK5yAGR7qGsDubTT9O7VpNQG9KIolCOov4xJU7scbmjGaFyYBm9c7ZIqbq6B+56YchPbtD0RewZC6CiF2w== +"@sentry/cli-linux-x64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.39.1.tgz#31fe008b02f92769543dc9919e2a5cbc4cda7889" + integrity sha512-IwayNZy+it7FWG4M9LayyUmG1a/8kT9+/IEm67sT5+7dkMIMcpmHDqL8rWcPojOXuTKaOBBjkVdNMBTXy0mXlA== -"@sentry/cli-win32-i686@2.38.1": - version "2.38.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.38.1.tgz#33c2390162ee5dbd2085918edcac528a677b6119" - integrity sha512-BVUM5y+ZDBK/LqyVvt0C7oolmg8aq7PI/u04/Pp6FLRExySqwyQim0vNyL2FRjIeX1yhbk7x4Z79UjEKqJBltA== +"@sentry/cli-win32-i686@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.39.1.tgz#609e8790c49414011445e397130560c777850b35" + integrity sha512-NglnNoqHSmE+Dz/wHeIVRnV2bLMx7tIn3IQ8vXGO5HWA2f8zYJGktbkLq1Lg23PaQmeZLPGlja3gBQfZYSG10Q== -"@sentry/cli-win32-x64@2.38.1": - version "2.38.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.38.1.tgz#e2dc089ed4ea8b40cd8aef51b526c340f3ee2542" - integrity sha512-+HgsdM3LFSzUNlDpicPRdTKfr5u+nJ2C5p4aDYPb2G+qoYW+66FI4NxgWSyzJsj3nVQ8lW5/6AoMP6U5z/e/0A== +"@sentry/cli-win32-x64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.39.1.tgz#1a874a5570c6d162b35d9d001c96e5389d07d2cb" + integrity sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw== -"@sentry/cli@^2.36.1": - version "2.38.1" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.38.1.tgz#a3c88c26526cf6f7ba3ef64e1e22dc669024abac" - integrity sha512-XFO04nP7cn0tboMQ4ALR81QRF/6xoWAFzNld7Io6jHbaFzihqewjxAqy7pSvVPaieepUjqe7m/Ippt00kKOACg== +"@sentry/cli@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.39.1.tgz#916bb5b7567ccf7fdf94ef6cf8a2b9ab78370d29" + integrity sha512-JIb3e9vh0+OmQ0KxmexMXg9oZsR/G7HMwxt5BUIKAXZ9m17Xll4ETXTRnRUBT3sf7EpNGAmlQk1xEmVN9pYZYQ== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -2770,51 +2990,34 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.38.1" - "@sentry/cli-linux-arm" "2.38.1" - "@sentry/cli-linux-arm64" "2.38.1" - "@sentry/cli-linux-i686" "2.38.1" - "@sentry/cli-linux-x64" "2.38.1" - "@sentry/cli-win32-i686" "2.38.1" - "@sentry/cli-win32-x64" "2.38.1" + "@sentry/cli-darwin" "2.39.1" + "@sentry/cli-linux-arm" "2.39.1" + "@sentry/cli-linux-arm64" "2.39.1" + "@sentry/cli-linux-i686" "2.39.1" + "@sentry/cli-linux-x64" "2.39.1" + "@sentry/cli-win32-i686" "2.39.1" + "@sentry/cli-win32-x64" "2.39.1" -"@sentry/core@8.38.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.38.0.tgz#5d1b74770c79e489e786018a3e514cddeb777bcb" - integrity sha512-sGD+5TEHU9G7X7zpyaoJxpOtwjTjvOd1f/MKBrWW2vf9UbYK+GUJrOzLhMoSWp/pHSYgvObkJkDb/HwieQjvhQ== - dependencies: - "@sentry/types" "8.38.0" - "@sentry/utils" "8.38.0" +"@sentry/core@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.43.0.tgz#e96a489e87a9999199f5ac27d8860da37c1fa8b4" + integrity sha512-ktyovtjkTMNud+kC/XfqHVCoQKreIKgx/hgeRvzPwuPyd1t1KzYmRL3DBkbcWVnyOPpVTHn+RsEI1eRcVYHtvw== "@sentry/react@^8.0.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.38.0.tgz#513cbd9ba35bb0258d10b74d272800cbc5f05631" - integrity sha512-5396tewO00wbJFHUkmU+ikmp4A+wuBpStNc7UDyAm642jfbPajj51+GWld/ZYNFiQaZ/8I9tvvpHqVLnUh21gg== + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.43.0.tgz#ad49bd16b0b1897613ef5cbd2f0a49b2b41f98a9" + integrity sha512-PsTzLrYio/FOJU537Y5Gj9jJi7OMHEjdttsC9INUxy5062LOd8ObtHsjE0mopLaSYEwUfSROQOBZCwmISh8ByQ== dependencies: - "@sentry/browser" "8.38.0" - "@sentry/core" "8.38.0" - "@sentry/types" "8.38.0" - "@sentry/utils" "8.38.0" + "@sentry/browser" "8.43.0" + "@sentry/core" "8.43.0" hoist-non-react-statics "^3.3.2" -"@sentry/types@8.38.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.38.0.tgz#9c48734a8b4055bfd553a0141efec78e9680ed09" - integrity sha512-fP5H9ZX01W4Z/EYctk3mkSHi7d06cLcX2/UWqwdWbyPWI+pL2QpUPICeO/C+8SnmYx//wFj3qWDhyPCh1PdFAA== - -"@sentry/utils@8.38.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.38.0.tgz#2f91ca7d044f6e17b993c866ca02a981c4c1bc25" - integrity sha512-3X7MgIKIx+2q5Al7QkhaRB4wV6DvzYsaeIwdqKUzGLuRjXmNgJrLoU87TAwQRmZ6Wr3IoEpThZZMNrzYPXxArw== - dependencies: - "@sentry/types" "8.38.0" - "@sentry/vite-plugin@^2.0.0": - version "2.22.6" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.22.6.tgz#d08a1ede05f137636d5b3c61845d24c0114f0d76" - integrity sha512-zIieP1VLWQb3wUjFJlwOAoaaJygJhXeUoGd0e/Ha2RLb2eW2S+4gjf6y6NqyY71tZ74LYVZKg/4prB6FAZSMXQ== + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.22.7.tgz#9b63452d1d8cd02e6ba6234395a611ae7656c67a" + integrity sha512-sYRNiNm4toQGq2BfZSJPdw36em3eQaLu+3NTFpA7Hl4g3Sp2Rt3CYObnW5bxlFEruRhxzvdyB383N9OefVZ6KA== dependencies: - "@sentry/bundler-plugin-core" "2.22.6" + "@sentry/bundler-plugin-core" "2.22.7" unplugin "1.0.1" "@snyk/github-codeowners@1.1.0": @@ -2923,6 +3126,19 @@ lz-string "^1.5.0" pretty-format "^27.0.2" +"@testing-library/jest-dom@^6.6.3": + version "6.6.3" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2" + integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" + redent "^3.0.0" + "@testing-library/react-hooks@^8.0.1": version "8.0.1" resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" @@ -2932,9 +3148,9 @@ react-error-boundary "^3.1.0" "@testing-library/react@^16.0.0": - version "16.0.1" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.1.tgz#29c0ee878d672703f5e7579f239005e4e0faa875" - integrity sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg== + version "16.1.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.1.0.tgz#aa0c61398bac82eaf89776967e97de41ac742d71" + integrity sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg== dependencies: "@babel/runtime" "^7.12.5" @@ -3056,12 +3272,12 @@ dependencies: undici-types "~6.19.8" -"@types/node@^20.0.0": - version "20.17.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.6.tgz#6e4073230c180d3579e8c60141f99efdf5df0081" - integrity sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ== +"@types/node@^22.0.0": + version "22.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.1.tgz#41ffeee127b8975a05f8c4f83fb89bcb2987d766" + integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== dependencies: - undici-types "~6.19.2" + undici-types "~6.20.0" "@types/normalize-package-data@^2.4.0": version "2.4.4" @@ -3086,11 +3302,9 @@ "@types/node" "*" "@types/react-dom@^18.3.0": - version "18.3.1" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" - integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== - dependencies: - "@types/react" "*" + version "18.3.3" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.3.tgz#3654138d0da1b0c7916f6ed0dc1cc2b576d47650" + integrity sha512-uTYkxTLkYp41nq/ULXyXMtkNT1vu5fXJoqad6uTNCOGat5t9cLgF4vMNLBXsTOXpdOI44XzKPY1M5RRm0bQHuw== "@types/react-router-dom@^5.3.3": version "5.3.3" @@ -3160,15 +3374,15 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.0.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz#7dc0e419c87beadc8f554bf5a42e5009ed3748dc" - integrity sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w== + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz#0901933326aea4443b81df3f740ca7dfc45c7bea" + integrity sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/type-utils" "8.14.0" - "@typescript-eslint/utils" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/scope-manager" "8.18.0" + "@typescript-eslint/type-utils" "8.18.0" + "@typescript-eslint/utils" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" @@ -3182,14 +3396,14 @@ "@typescript-eslint/utils" "5.62.0" "@typescript-eslint/parser@^8.0.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.14.0.tgz#0a7e9dbc11bc07716ab2d7b1226217e9f6b51fc8" - integrity sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA== + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.0.tgz#a1c9456cbb6a089730bf1d3fc47946c5fb5fe67b" + integrity sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q== dependencies: - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/typescript-estree" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/scope-manager" "8.18.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/typescript-estree" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.62.0": @@ -3200,21 +3414,21 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz#01f37c147a735cd78f0ff355e033b9457da1f373" - integrity sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw== +"@typescript-eslint/scope-manager@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz#30b040cb4557804a7e2bcc65cf8fdb630c96546f" + integrity sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw== dependencies: - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" -"@typescript-eslint/type-utils@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz#455c6af30c336b24a1af28bc4f81b8dd5d74d94d" - integrity sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ== +"@typescript-eslint/type-utils@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz#6f0d12cf923b6fd95ae4d877708c0adaad93c471" + integrity sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow== dependencies: - "@typescript-eslint/typescript-estree" "8.14.0" - "@typescript-eslint/utils" "8.14.0" + "@typescript-eslint/typescript-estree" "8.18.0" + "@typescript-eslint/utils" "8.18.0" debug "^4.3.4" ts-api-utils "^1.3.0" @@ -3223,10 +3437,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.14.0.tgz#0d33d8d0b08479c424e7d654855fddf2c71e4021" - integrity sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g== +"@typescript-eslint/types@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.0.tgz#3afcd30def8756bc78541268ea819a043221d5f3" + integrity sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -3241,13 +3455,13 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz#a7a3a5a53a6c09313e12fb4531d4ff582ee3c312" - integrity sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ== +"@typescript-eslint/typescript-estree@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz#d8ca785799fbb9c700cdff1a79c046c3e633c7f9" + integrity sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg== dependencies: - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -3269,15 +3483,15 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/utils@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.14.0.tgz#ac2506875e03aba24e602364e43b2dfa45529dbd" - integrity sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA== +"@typescript-eslint/utils@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.0.tgz#48f67205d42b65d895797bb7349d1be5c39a62f7" + integrity sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/typescript-estree" "8.14.0" + "@typescript-eslint/scope-manager" "8.18.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/typescript-estree" "8.18.0" "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" @@ -3287,13 +3501,13 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz#2418d5a54669af9658986ade4e6cfb7767d815ad" - integrity sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ== +"@typescript-eslint/visitor-keys@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz#7b6d33534fa808e33a19951907231ad2ea5c36dd" + integrity sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw== dependencies: - "@typescript-eslint/types" "8.14.0" - eslint-visitor-keys "^3.4.3" + "@typescript-eslint/types" "8.18.0" + eslint-visitor-keys "^4.2.0" "@ungap/structured-clone@^1.2.0": version "1.2.0" @@ -3312,17 +3526,15 @@ dependencies: "@use-gesture/core" "10.3.1" -"@vector-im/compound-design-tokens@^1.9.1": - version "1.9.2" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.9.2.tgz#0b76e5475da3bc36443f7dc87951b937b5013d6f" - integrity sha512-gQmK4dHR2iws3ZskDv8Il6A4/rvQV7TPSmEOXLsahDhBTInWqexXeQnNRSt9Z5DsLPrkxL3/KoCt9lfYu/yiag== - dependencies: - prettier "^3.3.3" +"@vector-im/compound-design-tokens@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.1.1.tgz#d6175a99fe4b97688464126f255386990f3048d6" + integrity sha512-QnUi2K14D9KTXxcLQKUU3V75cforZLMwhaaJDNftT8F5mG86950hAM+qhgDNEpEU+pkTffQj0/g/5859YmqWzQ== "@vector-im/compound-web@^7.2.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.3.0.tgz#9594113ac50bff4794715104a30a60c52d15517d" - integrity sha512-gDppQUtpk5LvNHUg+Zlv9qzo1iBAag0s3g8Ec0qS5q4zGBKG6ruXXrNUKg1aK8rpbo2hYQsGaHM6dD8NqLoq3Q== + version "7.4.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.4.0.tgz#a5af8af6346f8ff6c14c70f5d4eb2eab7357a7cc" + integrity sha512-ZRBUeEGNmj/fTkIRa8zGnyVN7ytowpfOtHChqNm+m/+OTJN3o/lOMuQHDV8jeSEW2YwPJqGvPuG/dRr89IcQkA== dependencies: "@floating-ui/react" "^0.26.24" "@radix-ui/react-context-menu" "^2.2.1" @@ -3336,25 +3548,25 @@ vaul "^1.0.0" "@vitejs/plugin-basic-ssl@^1.0.1": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz#8b840305a6b48e8764803435ec0c716fa27d3802" - integrity sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A== + version "1.2.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz#9490fe15b8833351982fbe0963987f69f40f5019" + integrity sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q== "@vitejs/plugin-react@^4.0.1": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz#28301ac6d7aaf20b73a418ee5c65b05519b4836c" - integrity sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA== + version "4.3.4" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz#c64be10b54c4640135a5b28a2432330e88ad7c20" + integrity sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug== dependencies: - "@babel/core" "^7.25.2" - "@babel/plugin-transform-react-jsx-self" "^7.24.7" - "@babel/plugin-transform-react-jsx-source" "^7.24.7" + "@babel/core" "^7.26.0" + "@babel/plugin-transform-react-jsx-self" "^7.25.9" + "@babel/plugin-transform-react-jsx-source" "^7.25.9" "@types/babel__core" "^7.20.5" react-refresh "^0.14.2" "@vitest/coverage-v8@^2.0.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-2.1.5.tgz#74ef3bf6737f9897a54af22f820d90e85883ff83" - integrity sha512-/RoopB7XGW7UEkUndRXF87A9CwkoZAJW01pj8/3pgmDVsjMH2IKy6H1A38po9tmUlwhSyYs0az82rbKd9Yaynw== + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-2.1.8.tgz#738527e6e79cef5004248452527e272e0df12284" + integrity sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw== dependencies: "@ampproject/remapping" "^2.3.0" "@bcoe/v8-coverage" "^0.2.3" @@ -3369,62 +3581,62 @@ test-exclude "^7.0.1" tinyrainbow "^1.2.0" -"@vitest/expect@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.5.tgz#5a6afa6314cae7a61847927bb5bc038212ca7381" - integrity sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q== +"@vitest/expect@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.8.tgz#13fad0e8d5a0bf0feb675dcf1d1f1a36a1773bc1" + integrity sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw== dependencies: - "@vitest/spy" "2.1.5" - "@vitest/utils" "2.1.5" + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" chai "^5.1.2" tinyrainbow "^1.2.0" -"@vitest/mocker@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.5.tgz#54ee50648bc0bb606dfc58e13edfacb8b9208324" - integrity sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ== +"@vitest/mocker@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.8.tgz#51dec42ac244e949d20009249e033e274e323f73" + integrity sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA== dependencies: - "@vitest/spy" "2.1.5" + "@vitest/spy" "2.1.8" estree-walker "^3.0.3" magic-string "^0.30.12" -"@vitest/pretty-format@2.1.5", "@vitest/pretty-format@^2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.5.tgz#bc79b8826d4a63dc04f2a75d2944694039fa50aa" - integrity sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw== +"@vitest/pretty-format@2.1.8", "@vitest/pretty-format@^2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz#88f47726e5d0cf4ba873d50c135b02e4395e2bca" + integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ== dependencies: tinyrainbow "^1.2.0" -"@vitest/runner@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.5.tgz#4d5e2ba2dfc0af74e4b0f9f3f8be020559b26ea9" - integrity sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g== +"@vitest/runner@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.8.tgz#b0e2dd29ca49c25e9323ea2a45a5125d8729759f" + integrity sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg== dependencies: - "@vitest/utils" "2.1.5" + "@vitest/utils" "2.1.8" pathe "^1.1.2" -"@vitest/snapshot@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.5.tgz#a09a8712547452a84e08b3ec97b270d9cc156b4f" - integrity sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg== +"@vitest/snapshot@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.8.tgz#d5dc204f4b95dc8b5e468b455dfc99000047d2de" + integrity sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg== dependencies: - "@vitest/pretty-format" "2.1.5" + "@vitest/pretty-format" "2.1.8" magic-string "^0.30.12" pathe "^1.1.2" -"@vitest/spy@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.5.tgz#f790d1394a5030644217ce73562e92465e83147e" - integrity sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw== +"@vitest/spy@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.8.tgz#bc41af3e1e6a41ae3b67e51f09724136b88fa447" + integrity sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg== dependencies: tinyspy "^3.0.2" -"@vitest/utils@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.5.tgz#0e19ce677c870830a1573d33ee86b0d6109e9546" - integrity sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg== +"@vitest/utils@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.8.tgz#f8ef85525f3362ebd37fd25d268745108d6ae388" + integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA== dependencies: - "@vitest/pretty-format" "2.1.5" + "@vitest/pretty-format" "2.1.8" loupe "^3.1.2" tinyrainbow "^1.2.0" @@ -3536,7 +3748,7 @@ aria-query@5.3.0: dependencies: dequal "^2.0.3" -aria-query@^5.3.2: +aria-query@^5.0.0, aria-query@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== @@ -3828,7 +4040,7 @@ broccoli-plugin@^4.0.7: rimraf "^3.0.2" symlink-or-copy "^1.3.1" -browserslist@^4.23.1, browserslist@^4.23.3, browserslist@^4.24.0: +browserslist@^4.23.1, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2: version "4.24.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== @@ -3894,11 +4106,16 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: +caniuse-lite@^1.0.30001646: version "1.0.30001680" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz#5380ede637a33b9f9f1fc6045ea99bd142f3da5e" integrity sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA== +caniuse-lite@^1.0.30001669: + version "1.0.30001687" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz#d0ac634d043648498eedf7a3932836beba90ebae" + integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -3932,6 +4149,14 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" @@ -3994,9 +4219,9 @@ chokidar@^4.0.0: readdirp "^4.0.1" ci-info@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.0.0.tgz#65466f8b280fc019b9f50a5388115d17a63a44f2" - integrity sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg== + version "4.1.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.1.0.tgz#92319d2fa29d2620180ea5afed31f589bc98cf83" + integrity sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A== classnames@^2.3.1, classnames@^2.5.1: version "2.5.1" @@ -4126,13 +4351,20 @@ copy-to-clipboard@^3.3.1: dependencies: toggle-selection "^1.0.6" -core-js-compat@^3.38.0, core-js-compat@^3.38.1: +core-js-compat@^3.38.0: version "3.38.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.1.tgz#2bc7a298746ca5a7bcb9c164bcb120f2ebc09a09" integrity sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw== dependencies: browserslist "^4.23.3" +core-js-compat@^3.38.1: + version "3.39.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.39.0.tgz#b12dccb495f2601dc860bdbe7b4e3ffa8ba63f61" + integrity sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw== + dependencies: + browserslist "^4.24.2" + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -4149,9 +4381,9 @@ cosmiconfig@^8.1.3: path-type "^4.0.0" cross-spawn@^7.0.0: - version "7.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" - integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -4203,6 +4435,11 @@ css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssdb@^8.2.1: version "8.2.1" resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.1.tgz#62a5d9a41e2c86f1d7c35981098fc5ce47c5766c" @@ -4270,10 +4507,10 @@ debounce@^1.2.1: resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.7: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: ms "^2.1.3" @@ -4291,6 +4528,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.2: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -4301,7 +4545,7 @@ decamelize@^5.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== -decimal.js@^10.4.3: +decimal.js@10, decimal.js@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== @@ -4397,6 +4641,11 @@ dom-accessibility-api@^0.5.9: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-accessibility-api@^0.6.3: + version "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@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -4436,9 +4685,9 @@ dot-case@^3.0.4: tslib "^2.0.3" dotenv@^16.3.1: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== eastasianwidth@^0.2.0: version "0.2.0" @@ -4455,9 +4704,9 @@ easy-table@1.2.0: wcwidth "^1.0.1" electron-to-chromium@^1.5.41: - version "1.5.62" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.62.tgz#8289468414b0b0b3e9180ef619a763555debe612" - integrity sha512-t8c+zLmJHa9dJy96yBZRXGQYoiCEnHYgFwn1asvSPZSUdVxnB62A4RASd7k41ytG3ErFBA0TpHlKg9D9SQBmLg== + version "1.5.72" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz#a732805986d3a5b5fedd438ddf4616c7d78ac2df" + integrity sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw== emoji-regex@^8.0.0: version "8.0.0" @@ -4687,6 +4936,36 @@ esbuild@^0.23.0: "@esbuild/win32-ia32" "0.23.0" "@esbuild/win32-x64" "0.23.0" +esbuild@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.0.tgz#f2d470596885fcb2e91c21eb3da3b3c89c0b55e7" + integrity sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.24.0" + "@esbuild/android-arm" "0.24.0" + "@esbuild/android-arm64" "0.24.0" + "@esbuild/android-x64" "0.24.0" + "@esbuild/darwin-arm64" "0.24.0" + "@esbuild/darwin-x64" "0.24.0" + "@esbuild/freebsd-arm64" "0.24.0" + "@esbuild/freebsd-x64" "0.24.0" + "@esbuild/linux-arm" "0.24.0" + "@esbuild/linux-arm64" "0.24.0" + "@esbuild/linux-ia32" "0.24.0" + "@esbuild/linux-loong64" "0.24.0" + "@esbuild/linux-mips64el" "0.24.0" + "@esbuild/linux-ppc64" "0.24.0" + "@esbuild/linux-riscv64" "0.24.0" + "@esbuild/linux-s390x" "0.24.0" + "@esbuild/linux-x64" "0.24.0" + "@esbuild/netbsd-x64" "0.24.0" + "@esbuild/openbsd-arm64" "0.24.0" + "@esbuild/openbsd-x64" "0.24.0" + "@esbuild/sunos-x64" "0.24.0" + "@esbuild/win32-arm64" "0.24.0" + "@esbuild/win32-ia32" "0.24.0" + "@esbuild/win32-x64" "0.24.0" + escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -4794,9 +5073,9 @@ eslint-plugin-matrix-org@^1.2.1: integrity sha512-A3cDjhG7RHwfCS8o3bOip8hSCsxtmgk2ahvqE5v/Ic2kPEZxixY6w8zLj7hFGsrRmPSEpLWqkVLt8uvQBapiQA== eslint-plugin-react-hooks@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101" - integrity sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw== + version "5.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz#3d34e37d5770866c34b87d5b499f5f0b53bf0854" + integrity sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw== eslint-plugin-react@^7.29.4: version "7.37.2" @@ -4838,9 +5117,9 @@ eslint-plugin-rxjs@^5.0.3: tsutils-etc "^1.4.1" eslint-plugin-unicorn@^56.0.0: - version "56.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.0.tgz#9fd3ebe6f478571734541fa745026b743175b59e" - integrity sha512-aXpddVz/PQMmd69uxO98PA4iidiVNvA0xOtbpUoz1WhBd4RxOQQYqN618v68drY0hmy5uU2jy1bheKEVWBjlPw== + version "56.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz#d10a3df69ba885939075bdc95a65a0c872e940d4" + integrity sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog== dependencies: "@babel/helper-validator-identifier" "^7.24.7" "@eslint-community/eslint-utils" "^4.4.0" @@ -4880,6 +5159,11 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + eslint@^8.14.0: version "8.57.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" @@ -5293,9 +5577,9 @@ globals@^13.19.0: type-fest "^0.20.2" globals@^15.9.0: - version "15.11.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-15.11.0.tgz#b96ed4c6998540c6fb824b24b5499216d2438d6e" - integrity sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw== + version "15.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.12.0.tgz#1811872883ad8f41055b61457a130221297de5b5" + integrity sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ== globalthis@^1.0.3, globalthis@^1.0.4: version "1.0.4" @@ -5480,9 +5764,9 @@ https-proxy-agent@^7.0.5: debug "4" i18next-browser-languagedetector@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz#b6fdd9b43af67c47f2c26c9ba27710a1eaf31e2f" - integrity sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw== + version "8.0.2" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz#037ca25c26877cad778f060a9e177054d9f8eaa3" + integrity sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g== dependencies: "@babel/runtime" "^7.23.2" @@ -5510,9 +5794,9 @@ i18next-parser@^9.0.0: vinyl-fs "^4.0.0" i18next@^23.0.0, i18next@^23.5.1: - version "23.16.5" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.16.5.tgz#53d48ae9f985fd73fc1fcb96e6c7d90ababf0831" - integrity sha512-KTlhE3EP9x6pPTAW7dy0WKIhoCpfOGhRQlO+jttQLgzVaoOjWwBWramu7Pp0i+8wDNduuzXfe3kkVbzrKyrbTA== + version "23.16.8" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.16.8.tgz#3ae1373d344c2393f465556f394aba5a9233b93a" + integrity sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg== dependencies: "@babel/runtime" "^7.23.2" @@ -5534,9 +5818,9 @@ ignore@^5.1.8, ignore@^5.2.0, ignore@^5.3.1: integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== immutable@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.2.tgz#bb8a987349a73efbe6b3b292a9cbaf1b530d296b" - integrity sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw== + version "5.0.3" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1" + integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw== import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" @@ -5881,9 +6165,9 @@ jackspeak@^3.1.2: "@pkgjs/parseargs" "^0.11.0" jiti@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.0.tgz#393d595fb6031a11d11171b5e4fc0b989ba3e053" - integrity sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g== + version "2.4.1" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.1.tgz#4de9766ccbfa941d9b6390d2b159a4b295a52e6b" + integrity sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -6010,9 +6294,9 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== knip@^5.27.2: - version "5.37.1" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.37.1.tgz#3c3e91c425dfb35be68b4d12cc0b9ee3cde794e8" - integrity sha512-69gjKj5lLsLXcIPXlHyFfX5AOHgRdh/iXH8gUqvmsHtjqoWhOATeXZDjvvemmgw7KxbWbUzxBNbpjhtJWzgqGA== + version "5.39.2" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.39.2.tgz#1faacd8d8ef36b509b2f6e396cce85b645abb04e" + integrity sha512-BuvuWRllLWV/r2G4m9ggNH+DZ6gouP/dhtJPXVlMbWNF++w9/EfrF6k2g7YBKCwjzCC+PXmYtpH8S2t8RjnY4Q== dependencies: "@nodelib/fs.walk" "1.2.8" "@snyk/github-codeowners" "1.1.0" @@ -6025,7 +6309,7 @@ knip@^5.27.2: picocolors "^1.1.0" picomatch "^4.0.1" pretty-ms "^9.0.0" - smol-toml "^1.3.0" + smol-toml "^1.3.1" strip-json-comments "5.0.1" summary "2.1.0" zod "^3.22.4" @@ -6067,12 +6351,12 @@ lines-and-columns@^1.1.6: integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== livekit-client@^2.5.7: - version "2.6.2" - resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.6.2.tgz#7821cac8d293b7685a4272b8aa269685f0ae75a8" - integrity sha512-SqXNHLgk2ZZOZyeHLXFAVAl+FVdSI+NK39LvIYstqS5X6IE5aCPlK4FqXY4l3aHpSft/BC/TR1CFGOq20ONkMA== + version "2.7.5" + resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.7.5.tgz#2c8e5956c1fda5844799f5a864ac87c803ca1a43" + integrity sha512-sPhHYwXvG75y1LDC50dDC9k6Z49L2vc/HcMRhzhi7yBca6ofPEebpB0bmPOry4ovrnFA+a8TL1pFR2mko1/clw== dependencies: "@livekit/mutex" "1.0.0" - "@livekit/protocol" "1.24.0" + "@livekit/protocol" "1.29.4" events "^3.3.0" loglevel "^1.8.0" sdp-transform "^2.14.1" @@ -6110,17 +6394,17 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loglevel@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7" integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg== -loglevel@^1.7.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" - integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== - -loglevel@^1.8.0, loglevel@^1.9.1: +loglevel@^1.7.1, loglevel@^1.8.0, loglevel@^1.9.1: version "1.9.2" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08" integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg== @@ -6174,9 +6458,9 @@ magic-string@0.30.8: "@jridgewell/sourcemap-codec" "^1.4.15" magic-string@^0.30.12: - version "0.30.12" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.12.tgz#9eb11c9d072b9bcb4940a5b2c2e1a217e4ee1a60" - integrity sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw== + version "0.30.15" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.15.tgz#d5474a2c4c5f35f041349edaba8a5cb02733ed3c" + integrity sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw== dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" @@ -6209,12 +6493,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@matrix-org/matrix-js-sdk#2210255d6ffc909c574fb8ef16f92140b2fb7797: - version "34.12.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2210255d6ffc909c574fb8ef16f92140b2fb7797" +matrix-js-sdk@matrix-org/matrix-js-sdk#develop: + version "34.13.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d1de32ea2773df4c6f8a956678bbd19b6d022475" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^11.0.0" "@matrix-org/olm" "3.2.15" another-json "^0.2.0" bs58 "^6.0.0" @@ -6349,9 +6633,9 @@ node-fetch@^2.6.7: whatwg-url "^5.0.0" node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-package-data@^2.5.0: version "2.5.0" @@ -6465,9 +6749,9 @@ observable-hooks@^4.2.3: integrity sha512-FdTQgyw1h5bG/QHCBIqctdBSnv9VARJCEilgpV6L2qlw1yeLqFIwPm4U15dMtl5kDmNN0hSt+Nl6iYbLFwEcQA== oidc-client-ts@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz#be264fb87c89f74f73863646431c32cd06f5ceb7" - integrity sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg== + version "3.1.0" + resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.1.0.tgz#28d3254951a1c64cc9780042c61492a71b2240dd" + integrity sha512-IDopEXjiwjkmJLYZo6BTlvwOtnlSniWZkKZoXforC/oLZHC9wkIxd25Kwtmo5yKFMMVcsp3JY6bhcNJqdYk8+g== dependencies: jwt-decode "^4.0.0" @@ -6949,7 +7233,7 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.41, postcss@^8.4.43: +postcss@^8.4.41, postcss@^8.4.43, postcss@^8.4.49: version "8.4.49" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== @@ -6977,10 +7261,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^3.0.0, prettier@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== +prettier@^3.0.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" + integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== pretty-format@^27.0.2: version "27.5.1" @@ -7102,9 +7386,9 @@ react-error-boundary@^3.1.0: "@babel/runtime" "^7.12.5" react-i18next@^15.0.0: - version "15.1.1" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.1.tgz#30bc76b39ded6ee37f1457677e46e6d6f11d9f64" - integrity sha512-R/Vg9wIli2P3FfeI8o1eNJUJue5LWpFsQePCHdQDmX0Co3zkr6kdT8gAseb/yGeWbNz1Txc4bKDQuZYsC0kQfw== + version "15.1.4" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.4.tgz#65c03c31a5e42202000652e163f22f23a9306a60" + integrity sha512-2tai71gmehbvl9ZIqPMqlCCkm/cbeV1G4STpmM3C8Uzo6T2l8jDvZxEVSsQKt8blP9X34iRFP/k1ROqG2296MQ== dependencies: "@babel/runtime" "^7.25.0" html-parse-stringify "^3.0.1" @@ -7261,6 +7545,14 @@ rechoir@^0.6.2: 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" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reflect.getprototypeof@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" @@ -7460,6 +7752,33 @@ rollup@^4.20.0: "@rollup/rollup-win32-x64-msvc" "4.25.0" fsevents "~2.3.2" +rollup@^4.23.0: + version "4.28.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.0.tgz#eb8d28ed43ef60a18f21d0734d230ee79dd0de77" + integrity sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.28.0" + "@rollup/rollup-android-arm64" "4.28.0" + "@rollup/rollup-darwin-arm64" "4.28.0" + "@rollup/rollup-darwin-x64" "4.28.0" + "@rollup/rollup-freebsd-arm64" "4.28.0" + "@rollup/rollup-freebsd-x64" "4.28.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.28.0" + "@rollup/rollup-linux-arm-musleabihf" "4.28.0" + "@rollup/rollup-linux-arm64-gnu" "4.28.0" + "@rollup/rollup-linux-arm64-musl" "4.28.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.28.0" + "@rollup/rollup-linux-riscv64-gnu" "4.28.0" + "@rollup/rollup-linux-s390x-gnu" "4.28.0" + "@rollup/rollup-linux-x64-gnu" "4.28.0" + "@rollup/rollup-linux-x64-musl" "4.28.0" + "@rollup/rollup-win32-arm64-msvc" "4.28.0" + "@rollup/rollup-win32-ia32-msvc" "4.28.0" + "@rollup/rollup-win32-x64-msvc" "4.28.0" + fsevents "~2.3.2" + rrweb-cssom@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b" @@ -7537,9 +7856,9 @@ safe-regex-test@^1.0.3: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.42.1: - version "1.81.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.81.0.tgz#a9010c0599867909dfdbad057e4a6fbdd5eec941" - integrity sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA== + version "1.82.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.82.0.tgz#30da277af3d0fa6042e9ceabd0d984ed6d07df70" + integrity sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q== dependencies: chokidar "^4.0.0" immutable "^5.0.2" @@ -7562,9 +7881,9 @@ scheduler@^0.23.2: loose-envify "^1.1.0" sdp-transform@^2.14.1: - version "2.14.2" - resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.14.2.tgz#d2cee6a1f7abe44e6332ac6cbb94e8600f32d813" - integrity sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA== + version "2.15.0" + resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.15.0.tgz#79d37a2481916f36a0534e07b32ceaa87f71df42" + integrity sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw== sdp@^3.2.0: version "3.2.0" @@ -7664,10 +7983,10 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -smol-toml@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.0.tgz#5200e251fffadbb72570c84e9776d2a3eca48143" - integrity sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA== +smol-toml@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.1.tgz#d9084a9e212142e3cab27ef4e2b8e8ba620bfe15" + integrity sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ== snake-case@^3.0.4: version "3.0.4" @@ -7698,9 +8017,9 @@ spdx-correct@^3.0.0: spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz#c07a4ede25b16e4f78e6707bbd84b15a45c19c1b" - integrity sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw== + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== spdx-expression-parse@^3.0.0: version "3.0.1" @@ -7711,9 +8030,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.16" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f" - integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw== + version "3.0.20" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89" + integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== sprintf-js@^1.1.1: version "1.1.3" @@ -8064,9 +8383,9 @@ tr46@~0.0.3: integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== ts-api-utils@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.0.tgz#709c6f2076e511a81557f3d07a0cbd566ae8195c" - integrity sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ== + version "1.4.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" + integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== ts-debounce@^4.0.0: version "4.0.0" @@ -8202,9 +8521,9 @@ typescript-eslint-language-service@^5.0.5: integrity sha512-b7gWXpwSTqMVKpPX3WttNZEyVAMKs/2jsHKF79H+qaD6mjzCyU5jboJe/lOZgLJD+QRsXCr0GjIVxvl5kI1NMw== typescript@^5.0.4, typescript@^5.1.6: - version "5.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" - integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== unbox-primitive@^1.0.2: version "1.0.2" @@ -8224,11 +8543,16 @@ underscore.string@~3.3.4: sprintf-js "^1.1.1" util-deprecate "^1.0.2" -undici-types@~6.19.2, undici-types@~6.19.8: +undici-types@~6.19.8: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + undici@^5.25.4: version "5.28.4" resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" @@ -8350,9 +8674,9 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== uuid@11: - version "11.0.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.2.tgz#a8d68ba7347d051e7ea716cc8dcbbab634d66875" - integrity sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ== + version "11.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d" + integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== validate-npm-package-license@^3.0.1: version "3.0.4" @@ -8373,9 +8697,9 @@ value-or-function@^4.0.0: integrity sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg== vaul@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.0.tgz#7da4bc965e0b184ada632f1208096b0f5575d920" - integrity sha512-YhO/bikcauk48hzhMhvIvT+U87cuCbNbKk9fF4Ou5UkI9t2KkBMernmdP37pCzF15hrv55fcny1YhexK8h6GVQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.1.tgz#93aceaad16f7c53aacf28a2609b2dd43b5a91fa0" + integrity sha512-+ejzF6ffQKPcfgS7uOrGn017g39F8SO4yLPXbBhpC7a0H+oPqPna8f1BUfXaz8eU4+pxbQcmjxW+jWBSbxjaFg== dependencies: "@radix-ui/react-dialog" "^1.1.1" @@ -8430,10 +8754,10 @@ vinyl@^3.0.0, vinyl@~3.0.0: replace-ext "^2.0.0" teex "^1.0.1" -vite-node@2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.5.tgz#cf28c637b2ebe65921f3118a165b7cf00a1cdf19" - integrity sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w== +vite-node@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5" + integrity sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg== dependencies: cac "^6.7.14" debug "^4.3.7" @@ -8442,14 +8766,12 @@ vite-node@2.1.5: vite "^5.0.0" vite-plugin-compression2@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/vite-plugin-compression2/-/vite-plugin-compression2-1.3.1.tgz#ac2a512f8ca90a76687add6cf441000dd2c41485" - integrity sha512-UMr66CFu+RVPiD8E3iaX9BdZjCgO+lzzaAPAZvL5YgwH6FU4OR/MulJEyp9wq9EKoO6ErjUtPpaiDi3hvzv79Q== + version "1.3.3" + resolved "https://registry.yarnpkg.com/vite-plugin-compression2/-/vite-plugin-compression2-1.3.3.tgz#d33ddfb4000c914783f4760f81a44ba52fc21ed1" + integrity sha512-Mb+xi/C5b68awtF4fNwRBPtoZiyUHU3I0SaBOAGlerlR31kusq1si6qG31lsjJH8T7QNg/p3IJY2HY9O9SvsfQ== dependencies: "@rollup/pluginutils" "^5.1.0" tar-mini "^0.2.0" - optionalDependencies: - vite "^5.3.4" vite-plugin-html-template@^1.1.0: version "1.2.2" @@ -8467,7 +8789,7 @@ vite-plugin-svgr@^4.0.0: "@svgr/core" "^8.1.0" "@svgr/plugin-jsx" "^8.1.0" -vite@^5.0.0, vite@^5.3.4: +vite@^5.0.0: version "5.4.11" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5" integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q== @@ -8478,6 +8800,17 @@ vite@^5.0.0, vite@^5.3.4: optionalDependencies: fsevents "~2.3.3" +vite@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.2.tgz#7a22630c73c7b663335ddcdb2390971ffbc14993" + integrity sha512-XdQ+VsY2tJpBsKGs0wf3U/+azx8BBpYRHFAyKm5VeEZNOJZRB63q7Sc8Iup3k0TrN3KO6QgyzFf+opSbfY1y0g== + dependencies: + esbuild "^0.24.0" + postcss "^8.4.49" + rollup "^4.23.0" + optionalDependencies: + fsevents "~2.3.3" + vitest-axe@^1.0.0-pre.3: version "1.0.0-pre.3" resolved "https://registry.yarnpkg.com/vitest-axe/-/vitest-axe-1.0.0-pre.3.tgz#0ea646c4ebe21c9b7ffb9ff3d6dff60b1c5a6124" @@ -8488,17 +8821,17 @@ vitest-axe@^1.0.0-pre.3: lodash-es "^4.17.21" vitest@^2.0.0: - version "2.1.5" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.5.tgz#a93b7b84a84650130727baae441354e6df118148" - integrity sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A== + version "2.1.8" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.8.tgz#2e6a00bc24833574d535c96d6602fb64163092fa" + integrity sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ== dependencies: - "@vitest/expect" "2.1.5" - "@vitest/mocker" "2.1.5" - "@vitest/pretty-format" "^2.1.5" - "@vitest/runner" "2.1.5" - "@vitest/snapshot" "2.1.5" - "@vitest/spy" "2.1.5" - "@vitest/utils" "2.1.5" + "@vitest/expect" "2.1.8" + "@vitest/mocker" "2.1.8" + "@vitest/pretty-format" "^2.1.8" + "@vitest/runner" "2.1.8" + "@vitest/snapshot" "2.1.8" + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" chai "^5.1.2" debug "^4.3.7" expect-type "^1.1.0" @@ -8510,7 +8843,7 @@ vitest@^2.0.0: tinypool "^1.0.1" tinyrainbow "^1.2.0" vite "^5.0.0" - vite-node "2.1.5" + vite-node "2.1.8" why-is-node-running "^2.3.0" void-elements@3.1.0: @@ -8812,6 +9145,6 @@ zod-validation-error@^3.0.3: integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ== zod@^3.22.4: - version "3.23.8" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" - integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + version "3.24.0" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.0.tgz#babb32313f7c5f4a99812feee806d186b4f76bde" + integrity sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w==