diff --git a/.github/workflows/build-and-publish-docker.yaml b/.github/workflows/build-and-publish-docker.yaml index dbde6c76..68f7131c 100644 --- a/.github/workflows/build-and-publish-docker.yaml +++ b/.github/workflows/build-and-publish-docker.yaml @@ -40,12 +40,50 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Connect to Tailscale + uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4 + if: github.event_name != 'pull_request' + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + audience: ${{ secrets.TS_AUDIENCE }} + tags: tag:github-actions + + - name: Compute vault jwt role name + id: vault-jwt-role + if: github.event_name != 'pull_request' + run: | + echo "role_name=github_service_management_$( echo "${{ github.repository }}" | sed -r 's|[/-]|_|g')" | tee -a "$GITHUB_OUTPUT" + + - name: Get team registry token + id: import-secrets + uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3 + if: github.event_name != 'pull_request' + with: + url: https://vault.infra.ci.i.element.dev + role: ${{ steps.vault-jwt-role.outputs.role_name }} + path: service-management/github-actions + jwtGithubAudience: https://vault.infra.ci.i.element.dev + method: jwt + secrets: | + services/-repositories/secret/data/oci.element.io username | OCI_USERNAME ; + services/-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; + + - name: Login to oci.element.io Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + if: github.event_name != 'pull_request' + with: + registry: oci-push.vpn.infra.element.io + username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} + password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }} + - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: ${{ inputs.docker_tags}} + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + oci-push.vpn.infra.element.io/element-web + tags: ${{ inputs.docker_tags }} labels: | org.opencontainers.image.licenses=AGPL-3.0-only OR LicenseRef-Element-Commercial diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index 28682a33..8d885399 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -47,7 +47,7 @@ services: - ecbackend livekit: - image: livekit/livekit-server:v1.9.4 + image: livekit/livekit-server:v1.9.11 pull_policy: always hostname: livekit-sfu command: --dev --config /etc/livekit.yaml @@ -67,7 +67,7 @@ services: - ecbackend livekit-1: - image: livekit/livekit-server:v1.9.4 + image: livekit/livekit-server:v1.9.11 pull_policy: always hostname: livekit-sfu-1 command: --dev --config /etc/livekit.yaml @@ -88,7 +88,7 @@ services: synapse: hostname: homeserver - image: ghcr.io/element-hq/synapse:pr-18968-dcb7678281bc02d4551043a6338fe5b7e6aa47ce + image: ghcr.io/element-hq/synapse:latest pull_policy: always environment: - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml @@ -106,7 +106,7 @@ services: synapse-1: hostname: homeserver-1 - image: ghcr.io/element-hq/synapse:pr-18968-dcb7678281bc02d4551043a6338fe5b7e6aa47ce + image: ghcr.io/element-hq/synapse:latest pull_policy: always environment: - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml diff --git a/embedded/android/gradle/libs.versions.toml b/embedded/android/gradle/libs.versions.toml index 5a91e19e..a93dc56e 100644 --- a/embedded/android/gradle/libs.versions.toml +++ b/embedded/android/gradle/libs.versions.toml @@ -2,11 +2,11 @@ # https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format [versions] -android_gradle_plugin = "8.13.1" +android_gradle_plugin = "8.13.2" [libraries] android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" } [plugins] android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" } -maven_publish = { id = "com.vanniktech.maven.publish", version = "0.35.0" } \ No newline at end of file +maven_publish = { id = "com.vanniktech.maven.publish", version = "0.36.0" } \ No newline at end of file diff --git a/embedded/android/gradle/wrapper/gradle-wrapper.properties b/embedded/android/gradle/wrapper/gradle-wrapper.properties index 7705927e..de413606 100644 --- a/embedded/android/gradle/wrapper/gradle-wrapper.properties +++ b/embedded/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/package.json b/package.json index 49612120..705b0f10 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@codecov/vite-plugin": "^1.3.0", "@fontsource/inconsolata": "^5.1.0", "@fontsource/inter": "^5.1.0", - "@formatjs/intl-durationformat": "^0.9.0", + "@formatjs/intl-durationformat": "^0.10.0", "@formatjs/intl-segmenter": "^11.7.3", "@livekit/components-core": "^0.12.0", "@livekit/components-react": "^2.0.0", @@ -101,7 +101,7 @@ "i18next-browser-languagedetector": "^8.0.0", "i18next-parser": "^9.1.0", "jsdom": "^26.0.0", - "knip": "^5.27.2", + "knip": "5.82.1", "livekit-client": "^2.13.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", @@ -118,7 +118,7 @@ "qrcode": "^1.5.4", "react": "19", "react-dom": "19", - "react-i18next": "^16.0.0 <16.1.0", + "react-i18next": "^16.0.0 <16.6.0", "react-router-dom": "^7.0.0", "react-use-measure": "^2.1.1", "rxjs": "^7.8.1", diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 61afb7b9..c19c4818 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -51,14 +51,7 @@ import { v4 as uuidv4 } from "uuid"; import { type IMembershipManager } from "matrix-js-sdk/lib/matrixrtc/IMembershipManager"; import { - LocalUserMediaViewModel, - type MediaViewModel, - type RemoteUserMediaViewModel, - ScreenShareViewModel, - type UserMediaViewModel, -} from "../MediaViewModel"; -import { - accumulate, + createToggle$, filterBehavior, generateItem, generateItems, @@ -92,8 +85,6 @@ import { type MuteStates } from "../MuteStates"; import { getUrlParams } from "../../UrlParams"; import { type ProcessorState } from "../../livekit/TrackProcessorContext"; import { ElementWidgetActions, widget } from "../../widget"; -import { UserMedia } from "../UserMedia.ts"; -import { ScreenShare } from "../ScreenShare.ts"; import { type GridLayoutMedia, type Layout, @@ -144,6 +135,14 @@ import { import { Publisher } from "./localMember/Publisher.ts"; import { type Connection } from "./remoteMembers/Connection.ts"; import { createLayoutModeSwitch } from "./LayoutSwitch.ts"; +import { + createWrappedUserMedia, + type MediaItem, + type WrappedUserMediaViewModel, +} from "../media/MediaItem.ts"; +import { type ScreenShareViewModel } from "../media/ScreenShareViewModel.ts"; +import { type UserMediaViewModel } from "../media/UserMediaViewModel.ts"; +import { type MediaViewModel } from "../media/MediaViewModel.ts"; const logger = rootLogger.getChild("[CallViewModel]"); //TODO @@ -193,7 +192,6 @@ interface LayoutScanState { tiles: TileStore; } -type MediaItem = UserMedia | ScreenShare; export type LivekitRoomItem = { livekitRoom: LivekitRoom; participants: string[]; @@ -283,7 +281,6 @@ export interface CallViewModel { allConnections$: Behavior; /** Participants sorted by livekit room so they can be used in the audio rendering */ livekitRoomItems$: Behavior; - userMedia$: Behavior; /** use the layout instead, this is just for the sdk export. */ matrixLivekitMembers$: Behavior; localMatrixLivekitMember$: Behavior; @@ -334,10 +331,6 @@ export interface CallViewModel { gridMode$: Behavior; setGridMode: (value: GridMode) => void; - // media view models and layout - grid$: Behavior; - spotlight$: Behavior; - pip$: Behavior; /** * The layout of tiles in the call interface. */ @@ -721,7 +714,7 @@ export function createCallViewModel$( /** * List of user media (camera feeds) that we want tiles for. */ - const userMedia$ = scope.behavior( + const userMedia$ = scope.behavior( combineLatest([ localMatrixLivekitMember$, matrixLivekitMembers$, @@ -767,36 +760,35 @@ export function createCallViewModel$( } } }, - (scope, _, dup, mediaId, userId, participant, connection$, rtcId) => { - const livekitRoom$ = scope.behavior( - connection$.pipe(map((c) => c?.livekitRoom)), - ); - const focusUrl$ = scope.behavior( - connection$.pipe(map((c) => c?.transport.livekit_service_url)), - ); - const displayName$ = scope.behavior( - matrixMemberMetadataStore - .createDisplayNameBehavior$(userId) - .pipe(map((name) => name ?? userId)), - ); - - return new UserMedia( - scope, - `${mediaId}:${dup}`, + (scope, _, dup, mediaId, userId, participant, connection$, rtcId) => + createWrappedUserMedia(scope, { + id: `${mediaId}:${dup}`, userId, - rtcId, + rtcBackendIdentity: rtcId, participant, - options.encryptionSystem, - livekitRoom$, - focusUrl$, + encryptionSystem: options.encryptionSystem, + livekitRoom$: scope.behavior( + connection$.pipe(map((c) => c?.livekitRoom)), + ), + focusUrl$: scope.behavior( + connection$.pipe(map((c) => c?.transport.livekit_service_url)), + ), mediaDevices, - localMembership.reconnecting$, - displayName$, - matrixMemberMetadataStore.createAvatarUrlBehavior$(userId), - handsRaised$.pipe(map((v) => v[mediaId]?.time ?? null)), - reactions$.pipe(map((v) => v[mediaId] ?? undefined)), - ); - }, + pretendToBeDisconnected$: localMembership.reconnecting$, + displayName$: scope.behavior( + matrixMemberMetadataStore + .createDisplayNameBehavior$(userId) + .pipe(map((name) => name ?? userId)), + ), + mxcAvatarUrl$: + matrixMemberMetadataStore.createAvatarUrlBehavior$(userId), + handRaised$: scope.behavior( + handsRaised$.pipe(map((v) => v[mediaId]?.time ?? null)), + ), + reaction$: scope.behavior( + reactions$.pipe(map((v) => v[mediaId] ?? undefined)), + ), + }), ), ), ); @@ -821,11 +813,9 @@ export function createCallViewModel$( /** * List of MediaItems that we want to display, that are of type ScreenShare */ - const screenShares$ = scope.behavior( + const screenShares$ = scope.behavior( mediaItems$.pipe( - map((mediaItems) => - mediaItems.filter((m): m is ScreenShare => m instanceof ScreenShare), - ), + map((mediaItems) => mediaItems.filter((m) => m.type === "screen share")), ), ); @@ -888,39 +878,39 @@ export function createCallViewModel$( merge(userHangup$, widgetHangup$).pipe(map(() => "user" as const)), ).pipe(scope.share); - const spotlightSpeaker$ = scope.behavior( + const spotlightSpeaker$ = scope.behavior( userMedia$.pipe( switchMap((mediaItems) => mediaItems.length === 0 ? of([]) : combineLatest( mediaItems.map((m) => - m.vm.speaking$.pipe(map((s) => [m, s] as const)), + m.speaking$.pipe(map((s) => [m, s] as const)), ), ), ), - scan<(readonly [UserMedia, boolean])[], UserMedia | undefined, null>( - (prev, mediaItems) => { - // Only remote users that are still in the call should be sticky - const [stickyMedia, stickySpeaking] = - (!prev?.vm.local && mediaItems.find(([m]) => m === prev)) || []; - // Decide who to spotlight: - // If the previous speaker is still speaking, stick with them rather - // than switching eagerly to someone else - return stickySpeaking - ? stickyMedia! - : // Otherwise, select any remote user who is speaking - (mediaItems.find(([m, s]) => !m.vm.local && s)?.[0] ?? - // Otherwise, stick with the person who was last speaking - stickyMedia ?? - // 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]); - }, - null, - ), - map((speaker) => speaker?.vm ?? null), + scan< + (readonly [UserMediaViewModel, boolean])[], + UserMediaViewModel | undefined, + undefined + >((prev, mediaItems) => { + // Only remote users that are still in the call should be sticky + const [stickyMedia, stickySpeaking] = + (!prev?.local && mediaItems.find(([m]) => m === prev)) || []; + // Decide who to spotlight: + // If the previous speaker is still speaking, stick with them rather + // than switching eagerly to someone else + return stickySpeaking + ? stickyMedia! + : // Otherwise, select any remote user who is speaking + (mediaItems.find(([m, s]) => !m.local && s)?.[0] ?? + // Otherwise, stick with the person who was last speaking + stickyMedia ?? + // Otherwise, spotlight an arbitrary remote user + mediaItems.find(([m]) => !m.local)?.[0] ?? + // Otherwise, spotlight the local user + mediaItems.find(([m]) => m.local)?.[0]); + }, undefined), ), ); @@ -934,7 +924,7 @@ export function createCallViewModel$( return bins.length === 0 ? of([]) : combineLatest(bins, (...bins) => - bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm), + bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m), ); }), distinctUntilChanged(shallowEquals), @@ -944,9 +934,7 @@ export function createCallViewModel$( const spotlight$ = scope.behavior( screenShares$.pipe( switchMap((screenShares) => { - if (screenShares.length > 0) { - return of(screenShares.map((m) => m.vm)); - } + if (screenShares.length > 0) return of(screenShares); return spotlightSpeaker$.pipe( map((speaker) => (speaker ? [speaker] : [])), @@ -956,7 +944,7 @@ export function createCallViewModel$( ), ); - const pip$ = scope.behavior( + const pip$ = scope.behavior( combineLatest([ // TODO This also needs epoch logic to dedupe the screenshares and mediaItems emits screenShares$, @@ -968,28 +956,17 @@ export function createCallViewModel$( return spotlightSpeaker$; } if (!spotlight || spotlight.local) { - return of(null); + return of(undefined); } const localUserMedia = mediaItems.find( - (m) => m.vm instanceof LocalUserMediaViewModel, - ) as UserMedia | undefined; - - const localUserMediaViewModel = localUserMedia?.vm as - | LocalUserMediaViewModel - | undefined; - - if (!localUserMediaViewModel) { - return of(null); + (m) => m.type === "user" && m.local, + ); + if (!localUserMedia) { + return of(undefined); } - return localUserMediaViewModel.alwaysShow$.pipe( - map((alwaysShow) => { - if (alwaysShow) { - return localUserMediaViewModel; - } - - return null; - }), + return localUserMedia.alwaysShow$.pipe( + map((alwaysShow) => (alwaysShow ? localUserMedia : undefined)), ); }), ), @@ -998,7 +975,7 @@ export function createCallViewModel$( const hasRemoteScreenShares$ = scope.behavior( spotlight$.pipe( map((spotlight) => - spotlight.some((vm) => !vm.local && vm instanceof ScreenShareViewModel), + spotlight.some((vm) => vm.type === "screen share" && !vm.local), ), ), ); @@ -1039,8 +1016,10 @@ export function createCallViewModel$( ); const spotlightExpandedToggle$ = new Subject(); - const spotlightExpanded$ = scope.behavior( - spotlightExpandedToggle$.pipe(accumulate(false, (expanded) => !expanded)), + const spotlightExpanded$ = createToggle$( + scope, + false, + spotlightExpandedToggle$, ); const { setGridMode, gridMode$ } = createLayoutModeSwitch( @@ -1053,7 +1032,7 @@ export function createCallViewModel$( [grid$, spotlight$], (grid, spotlight) => ({ type: "grid", - spotlight: spotlight.some((vm) => vm instanceof ScreenShareViewModel) + spotlight: spotlight.some((vm) => vm.type === "screen share") ? spotlight : undefined, grid, @@ -1085,12 +1064,8 @@ export function createCallViewModel$( mediaItems$.pipe( map((mediaItems) => { if (mediaItems.length !== 2) return null; - const local = mediaItems.find((vm) => vm.vm.local)?.vm as - | LocalUserMediaViewModel - | undefined; - const remote = mediaItems.find((vm) => !vm.vm.local)?.vm as - | RemoteUserMediaViewModel - | undefined; + const local = mediaItems.find((vm) => vm.type === "user" && vm.local); + const remote = mediaItems.find((vm) => vm.type === "user" && !vm.local); // 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 @@ -1138,7 +1113,7 @@ export function createCallViewModel$( oneOnOne === null ? combineLatest([grid$, spotlight$], (grid, spotlight) => grid.length > smallMobileCallThreshold || - spotlight.some((vm) => vm instanceof ScreenShareViewModel) + spotlight.some((vm) => vm.type === "screen share") ? spotlightPortraitLayoutMedia$ : gridLayoutMedia$, ).pipe(switchAll()) @@ -1245,7 +1220,7 @@ export function createCallViewModel$( // screen sharing feeds are in the spotlight we still need them. return l.spotlight.media$.pipe( map((models: MediaViewModel[]) => - models.some((m) => m instanceof ScreenShareViewModel), + models.some((m) => m.type === "screen share"), ), ); // In expanded spotlight layout, the active speaker is always shown in @@ -1552,11 +1527,7 @@ export function createCallViewModel$( toggleSpotlightExpanded$: toggleSpotlightExpanded$, gridMode$: gridMode$, setGridMode: setGridMode, - grid$: grid$, - spotlight$: spotlight$, - pip$: pip$, layout$: layout$, - userMedia$, localMatrixLivekitMember$, matrixLivekitMembers$: scope.behavior( matrixLivekitMembers$.pipe( diff --git a/src/state/CallViewModel/localMember/Publisher.ts b/src/state/CallViewModel/localMember/Publisher.ts index 8df38743..b7841c49 100644 --- a/src/state/CallViewModel/localMember/Publisher.ts +++ b/src/state/CallViewModel/localMember/Publisher.ts @@ -30,7 +30,7 @@ import { trackProcessorSync, } from "../../../livekit/TrackProcessorContext.tsx"; import { getUrlParams } from "../../../UrlParams.ts"; -import { observeTrackReference$ } from "../../MediaViewModel.ts"; +import { observeTrackReference$ } from "../../observeTrackReference"; import { type Connection } from "../remoteMembers/Connection.ts"; import { ObservableScope } from "../../ObservableScope.ts"; diff --git a/src/state/MediaViewModel.ts b/src/state/MediaViewModel.ts deleted file mode 100644 index 57b0428a..00000000 --- a/src/state/MediaViewModel.ts +++ /dev/null @@ -1,850 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { - type AudioSource, - type VideoSource, - type TrackReference, - observeParticipantEvents, - observeParticipantMedia, - roomEventSelector, -} from "@livekit/components-core"; -import { - type LocalParticipant, - LocalTrack, - LocalVideoTrack, - type Participant, - ParticipantEvent, - type RemoteParticipant, - Track, - TrackEvent, - facingModeFromLocalTrack, - type Room as LivekitRoom, - RoomEvent as LivekitRoomEvent, - RemoteTrack, -} from "livekit-client"; -import { logger } from "matrix-js-sdk/lib/logger"; -import { - BehaviorSubject, - type Observable, - Subject, - combineLatest, - filter, - fromEvent, - interval, - map, - merge, - of, - startWith, - switchMap, - throttleTime, - distinctUntilChanged, - concat, - take, -} from "rxjs"; - -import { alwaysShowSelf } from "../settings/settings"; -import { showConnectionStats } from "../settings/settings"; -import { accumulate } from "../utils/observable"; -import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; -import { E2eeType } from "../e2ee/e2eeType"; -import { type ReactionOption } from "../reactions"; -import { platform } from "../Platform"; -import { type MediaDevices } from "./MediaDevices"; -import { type Behavior } from "./Behavior"; -import { type ObservableScope } from "./ObservableScope"; -import { videoFit$, videoSizeFromParticipant$ } from "../utils/videoFit.ts"; - -export function observeTrackReference$( - participant: Participant, - source: Track.Source, -): Observable { - return observeParticipantMedia(participant).pipe( - map(() => participant.getTrackPublication(source)), - distinctUntilChanged(), - map((publication) => publication && { participant, publication, source }), - ); -} - -/** - * Helper function to observe the RTC stats for a given participant and track source. - * It polls the stats every second and emits the latest stats object. - */ -export function observeRtpStreamStats$( - participant: Participant, - source: Track.Source, - type: "inbound-rtp" | "outbound-rtp", -): Observable< - RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined -> { - return combineLatest([ - observeTrackReference$(participant, source), - // This is used also for detecting video orientation, - // and we want that to be more responsive than the connection stats, so we poll more frequently at the start. - concat(interval(300).pipe(take(3)), interval(1000)).pipe(startWith(0)), - ]).pipe( - switchMap(async ([trackReference]) => { - const track = trackReference?.publication?.track; - if ( - !track || - !(track instanceof RemoteTrack || track instanceof LocalTrack) - ) { - return undefined; - } - const report = await track.getRTCStatsReport(); - if (!report) { - return undefined; - } - for (const v of report.values()) { - if (v.type === type) { - return v; - } - } - - return undefined; - }), - startWith(undefined), - ); -} - -/** - * Helper function to observe the inbound RTP stats for a given participant and track source. - * To be used for remote participants' audio and video tracks. - * It polls the stats every second and emits the latest stats object. - * @param participant - The LiveKit participant whose track stats we want to observe. - * @param source - The source of the track (e.g. Track.Source.Camera or Track.Source.Microphone). - */ -export function observeInboundRtpStreamStats$( - participant: Participant, - source: Track.Source, -): Observable { - return observeRtpStreamStats$(participant, source, "inbound-rtp").pipe( - map((x) => x as RTCInboundRtpStreamStats | undefined), - ); -} - -/** - * Helper function to observe the outbound RTP stats for a given participant and track source. - * To be used for the local participant's audio and video tracks. - * It polls the stats every second and emits the latest stats object. - * @param participant - The LiveKit participant whose track stats we want to observe. - * @param source - The source of the track (e.g. Track.Source.Camera or Track.Source.Microphone). - */ -export function observeOutboundRtpStreamStats$( - participant: Participant, - source: Track.Source, -): Observable { - return observeRtpStreamStats$(participant, source, "outbound-rtp").pipe( - map((x) => x as RTCOutboundRtpStreamStats | undefined), - ); -} - -function observeRemoteTrackReceivingOkay$( - participant: Participant, - source: Track.Source, -): Observable { - let lastStats: { - framesDecoded: number | undefined; - framesDropped: number | undefined; - framesReceived: number | undefined; - } = { - framesDecoded: undefined, - framesDropped: undefined, - framesReceived: undefined, - }; - - return observeInboundRtpStreamStats$(participant, source).pipe( - map((stats) => { - if (!stats) return undefined; - const { framesDecoded, framesDropped, framesReceived } = stats; - return { - framesDecoded, - framesDropped, - framesReceived, - }; - }), - filter((newStats) => !!newStats), - map((newStats): boolean | undefined => { - const oldStats = lastStats; - lastStats = newStats; - if ( - typeof newStats.framesReceived === "number" && - typeof oldStats.framesReceived === "number" && - typeof newStats.framesDecoded === "number" && - typeof oldStats.framesDecoded === "number" - ) { - const framesReceivedDelta = - newStats.framesReceived - oldStats.framesReceived; - const framesDecodedDelta = - newStats.framesDecoded - oldStats.framesDecoded; - - // if we received >0 frames and managed to decode >0 frames then we treat that as success - - if (framesReceivedDelta > 0) { - return framesDecodedDelta > 0; - } - } - - // no change - return undefined; - }), - filter((x) => typeof x === "boolean"), - startWith(undefined), - ); -} - -function encryptionErrorObservable$( - room$: Behavior, - participant: Participant, - encryptionSystem: EncryptionSystem, - criteria: string, -): Observable { - return room$.pipe( - switchMap((room) => { - if (room === undefined) return of(false); - return roomEventSelector(room, LivekitRoomEvent.EncryptionError).pipe( - map((e) => { - const [err] = e; - if (encryptionSystem.kind === E2eeType.PER_PARTICIPANT) { - return ( - // Ideally we would pull the participant identity from the field on the error. - // However, it gets lost in the serialization process between workers. - // So, instead we do a string match - (err?.message.includes(participant.identity) && - err?.message.includes(criteria)) ?? - false - ); - } else if (encryptionSystem.kind === E2eeType.SHARED_KEY) { - return !!err?.message.includes(criteria); - } - - return false; - }), - ); - }), - distinctUntilChanged(), - throttleTime(1000), // Throttle to avoid spamming the UI - startWith(false), - ); -} - -export enum EncryptionStatus { - Connecting, - Okay, - KeyMissing, - KeyInvalid, - PasswordInvalid, -} - -abstract class BaseMediaViewModel { - /** - * The LiveKit video track for this media. - */ - public readonly video$: Behavior; - /** - * Whether there should be a warning that this media is unencrypted. - */ - public readonly unencryptedWarning$: Behavior; - - public readonly encryptionStatus$: Behavior; - - /** - * Whether this media corresponds to the local participant. - */ - public abstract readonly local: boolean; - - private observeTrackReference$( - source: Track.Source, - ): Behavior { - return this.scope.behavior( - this.participant$.pipe( - switchMap((p) => - !p ? of(undefined) : observeTrackReference$(p, source), - ), - ), - ); - } - - public constructor( - protected readonly scope: ObservableScope, - /** - * An opaque identifier for this media. - */ - public readonly id: string, - /** - * The Matrix user to which this media belongs. - */ - public readonly userId: string, - public readonly rtcBackendIdentity: string, - // We don't necessarily have a participant if a user connects via MatrixRTC but not (yet) through - // livekit. - protected readonly participant$: Observable< - LocalParticipant | RemoteParticipant | null - >, - encryptionSystem: EncryptionSystem, - audioSource: AudioSource, - videoSource: VideoSource, - protected readonly livekitRoom$: Behavior, - public readonly focusUrl$: Behavior, - public readonly displayName$: Behavior, - public readonly mxcAvatarUrl$: Behavior, - ) { - const audio$ = this.observeTrackReference$(audioSource); - this.video$ = this.observeTrackReference$(videoSource); - - this.unencryptedWarning$ = this.scope.behavior( - combineLatest( - [audio$, this.video$], - (a, v) => - encryptionSystem.kind !== E2eeType.NONE && - (a?.publication.isEncrypted === false || - v?.publication.isEncrypted === false), - ), - ); - - this.encryptionStatus$ = this.scope.behavior( - 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), - ); - } - }), - ), - ); - } -} - -/** - * Some participant's media. - */ -export type MediaViewModel = UserMediaViewModel | ScreenShareViewModel; -export type UserMediaViewModel = - | LocalUserMediaViewModel - | RemoteUserMediaViewModel; - -/** - * Some participant's user media. - */ -abstract class BaseUserMediaViewModel extends BaseMediaViewModel { - private readonly _speaking$ = this.scope.behavior( - this.participant$.pipe( - switchMap((p) => - p - ? observeParticipantEvents( - p, - ParticipantEvent.IsSpeakingChanged, - ).pipe(map((p) => p.isSpeaking)) - : of(false), - ), - ), - ); - /** - * Whether the participant is speaking. - */ - // Getter backed by a private field so that subclasses can override it - public get speaking$(): Behavior { - return this._speaking$; - } - - /** - * Whether this participant is sending audio (i.e. is unmuted on their side). - */ - public readonly audioEnabled$: Behavior; - - private readonly _videoEnabled$: Behavior; - /** - * Whether this participant is sending video. - */ - // Getter backed by a private field so that subclasses can override it - public get videoEnabled$(): Behavior { - return this._videoEnabled$; - } - - /** - * Whether the tile video should be contained inside the tile (video-fit contain) or be cropped to fit (video-fit cover). - */ - public readonly videoFit$: Behavior<"cover" | "contain">; - - protected constructor( - scope: ObservableScope, - id: string, - userId: string, - rtcBackendIdentity: string, - participant$: Observable, - encryptionSystem: EncryptionSystem, - livekitRoom$: Behavior, - focusUrl$: Behavior, - displayName$: Behavior, - mxcAvatarUrl$: Behavior, - public readonly handRaised$: Behavior, - public readonly reaction$: Behavior, - ) { - super( - scope, - id, - userId, - rtcBackendIdentity, - participant$, - encryptionSystem, - Track.Source.Microphone, - Track.Source.Camera, - livekitRoom$, - focusUrl$, - displayName$, - mxcAvatarUrl$, - ); - - const media$ = this.scope.behavior( - participant$.pipe( - switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)), - ), - ); - this.audioEnabled$ = this.scope.behavior( - media$.pipe(map((m) => m?.microphoneTrack?.isMuted === false)), - ); - this._videoEnabled$ = this.scope.behavior( - media$.pipe(map((m) => m?.cameraTrack?.isMuted === false)), - ); - - this.videoFit$ = videoFit$( - this.scope, - videoSizeFromParticipant$(participant$), - this.actualSize$, - ); - } - - public get local(): boolean { - return this instanceof LocalUserMediaViewModel; - } - - public abstract get audioStreamStats$(): Observable< - RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined - >; - - public abstract get videoStreamStats$(): Observable< - RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined - >; - - private readonly _actualSize$ = new BehaviorSubject< - { width: number; height: number } | undefined - >(undefined); - public readonly actualSize$ = this._actualSize$.asObservable(); - - /** - * Set the actual dimensions of the html element. - * This can be used to determine the best video fit (fit to frame / keep ratio). - * @param width - The actual width of the html element displaying the video. - * @param height - The actual height of the html element displaying the video. - */ - public setActualDimensions(width: number, height: number): void { - this._actualSize$.next({ - width, - height, - }); - } -} - -/** - * The local participant's user media. - */ -export class LocalUserMediaViewModel extends BaseUserMediaViewModel { - /** - * The local video track as an observable that emits whenever the track - * changes, the camera is switched, or the track is muted. - */ - private readonly videoTrack$: Observable = - this.video$.pipe( - switchMap((v) => { - const track = v?.publication.track; - if (!(track instanceof LocalVideoTrack)) return of(null); - return merge( - // Watch for track restarts because they indicate a camera switch. - // This event is also emitted when unmuting the track object. - fromEvent(track, TrackEvent.Restarted).pipe( - startWith(null), - map(() => track), - ), - // When the track object is muted, reset it to null. - fromEvent(track, TrackEvent.Muted).pipe(map(() => null)), - ); - }), - ); - - /** - * Whether the video should be mirrored. - */ - public readonly mirror$ = this.scope.behavior( - this.videoTrack$.pipe( - // Mirror only front-facing cameras (those that face the user) - map( - (track) => - track !== null && - facingModeFromLocalTrack(track).facingMode === "user", - ), - ), - ); - - /** - * Whether to show this tile in a highly visible location near the start of - * the grid. - */ - public readonly alwaysShow$ = alwaysShowSelf.value$; - public readonly setAlwaysShow = alwaysShowSelf.setValue; - - /** - * Callback for switching between the front and back cameras. - */ - public readonly switchCamera$: Behavior<(() => void) | null> = - this.scope.behavior( - platform === "desktop" - ? of(null) - : this.videoTrack$.pipe( - map((track) => { - if (track === null) return null; - const facingMode = facingModeFromLocalTrack(track).facingMode; - // If the camera isn't front or back-facing, don't provide a switch - // camera shortcut at all - if (facingMode !== "user" && facingMode !== "environment") - return null; - // Restart the track with a camera facing the opposite direction - return (): void => - void track - .restartTrack({ - facingMode: facingMode === "user" ? "environment" : "user", - }) - .then(() => { - // Inform the MediaDevices which camera was chosen - const deviceId = - track.mediaStreamTrack.getSettings().deviceId; - if (deviceId !== undefined) - this.mediaDevices.videoInput.select(deviceId); - }) - .catch((e) => - logger.error("Failed to switch camera", facingMode, e), - ); - }), - ), - ); - - public constructor( - scope: ObservableScope, - id: string, - userId: string, - rtcBackendIdentity: string, - participant$: Behavior, - encryptionSystem: EncryptionSystem, - livekitRoom$: Behavior, - focusUrl$: Behavior, - private readonly mediaDevices: MediaDevices, - displayName$: Behavior, - mxcAvatarUrl$: Behavior, - handRaised$: Behavior, - reaction$: Behavior, - ) { - super( - scope, - id, - userId, - rtcBackendIdentity, - participant$, - encryptionSystem, - livekitRoom$, - focusUrl$, - displayName$, - mxcAvatarUrl$, - handRaised$, - reaction$, - ); - } - - public audioStreamStats$ = combineLatest([ - this.participant$, - showConnectionStats.value$, - ]).pipe( - switchMap(([p, showConnectionStats]) => { - if (!p || !showConnectionStats) return of(undefined); - return observeOutboundRtpStreamStats$(p, Track.Source.Microphone); - }), - ); - - public videoStreamStats$ = combineLatest([ - this.participant$, - showConnectionStats.value$, - ]).pipe( - switchMap(([p, showConnectionStats]) => { - if (!p || !showConnectionStats) return of(undefined); - return observeOutboundRtpStreamStats$(p, Track.Source.Camera); - }), - ); -} - -/** - * A remote participant's user media. - */ -export class RemoteUserMediaViewModel extends BaseUserMediaViewModel { - /** - * Whether we are waiting for this user's LiveKit participant to exist. This - * could be because either we or the remote party are still connecting. - */ - public readonly waitingForMedia$ = this.scope.behavior( - combineLatest( - [this.livekitRoom$, this.participant$], - (livekitRoom, participant) => - // If livekitRoom is undefined, the user is not attempting to publish on - // any transport and so we shouldn't expect a participant. (They might - // be a subscribe-only bot for example.) - livekitRoom !== undefined && participant === null, - ), - ); - - // This private field is used to override the value from the superclass - private __speaking$: Behavior; - - public get speaking$(): Behavior { - return this.__speaking$; - } - - private readonly locallyMutedToggle$ = new Subject(); - private readonly localVolumeAdjustment$ = new Subject(); - private readonly localVolumeCommit$ = new Subject(); - - /** - * The volume to which this participant's audio is set, as a scalar - * multiplier. - */ - public readonly localVolume$ = this.scope.behavior( - merge( - this.locallyMutedToggle$.pipe(map(() => "toggle mute" as const)), - this.localVolumeAdjustment$, - this.localVolumeCommit$.pipe(map(() => "commit" as const)), - ).pipe( - accumulate({ volume: 1, committedVolume: 1 }, (state, event) => { - switch (event) { - case "toggle mute": - return { - ...state, - volume: state.volume === 0 ? state.committedVolume : 0, - }; - case "commit": - // Dragging the slider to zero should have the same effect as - // muting: keep the original committed volume, as if it were never - // dragged - return { - ...state, - committedVolume: - state.volume === 0 ? state.committedVolume : state.volume, - }; - default: - // Volume adjustment - return { ...state, volume: event }; - } - }), - map(({ volume }) => volume), - ), - ); - - // This private field is used to override the value from the superclass - private __videoEnabled$: Behavior; - - public get videoEnabled$(): Behavior { - return this.__videoEnabled$; - } - - /** - * Whether this participant's audio is disabled. - */ - public readonly locallyMuted$ = this.scope.behavior( - this.localVolume$.pipe(map((volume) => volume === 0)), - ); - - public constructor( - scope: ObservableScope, - id: string, - userId: string, - rtcBackendIdentity: string, - participant$: Observable, - encryptionSystem: EncryptionSystem, - livekitRoom$: Behavior, - focusUrl$: Behavior, - private readonly pretendToBeDisconnected$: Behavior, - displayName$: Behavior, - mxcAvatarUrl$: Behavior, - handRaised$: Behavior, - reaction$: Behavior, - ) { - super( - scope, - id, - userId, - rtcBackendIdentity, - participant$, - encryptionSystem, - livekitRoom$, - focusUrl$, - displayName$, - mxcAvatarUrl$, - handRaised$, - reaction$, - ); - - this.__speaking$ = this.scope.behavior( - pretendToBeDisconnected$.pipe( - switchMap((disconnected) => - disconnected ? of(false) : super.speaking$, - ), - ), - ); - - this.__videoEnabled$ = this.scope.behavior( - pretendToBeDisconnected$.pipe( - switchMap((disconnected) => - disconnected ? of(false) : super.videoEnabled$, - ), - ), - ); - - // Sync the local volume with LiveKit - combineLatest([ - participant$, - // The local volume, taking into account whether we're supposed to pretend - // that the audio stream is disconnected (since we don't necessarily want - // that to modify the UI state). - this.pretendToBeDisconnected$.pipe( - switchMap((disconnected) => (disconnected ? of(0) : this.localVolume$)), - this.scope.bind(), - ), - ]).subscribe(([p, volume]) => p?.setVolume(volume)); - } - - public toggleLocallyMuted(): void { - this.locallyMutedToggle$.next(); - } - - public setLocalVolume(value: number): void { - this.localVolumeAdjustment$.next(value); - } - - public commitLocalVolume(): void { - this.localVolumeCommit$.next(); - } - - public audioStreamStats$ = combineLatest([ - this.participant$, - showConnectionStats.value$, - ]).pipe( - switchMap(([p, showConnectionStats]) => { - if (!p || !showConnectionStats) return of(undefined); - return observeInboundRtpStreamStats$(p, Track.Source.Microphone); - }), - ); - - public videoStreamStats$ = combineLatest([ - this.participant$, - showConnectionStats.value$, - ]).pipe( - switchMap(([p, showConnectionStats]) => { - if (!p || !showConnectionStats) return of(undefined); - return observeInboundRtpStreamStats$(p, Track.Source.Camera); - }), - ); -} - -/** - * Some participant's screen share media. - */ -export class ScreenShareViewModel extends BaseMediaViewModel { - /** - * Whether this screen share's video should be displayed. - */ - public readonly videoEnabled$ = this.scope.behavior( - this.pretendToBeDisconnected$.pipe(map((disconnected) => !disconnected)), - ); - - public constructor( - scope: ObservableScope, - id: string, - userId: string, - rtcBackendIdentity: string, - participant$: Observable, - encryptionSystem: EncryptionSystem, - livekitRoom$: Behavior, - focusUrl$: Behavior, - private readonly pretendToBeDisconnected$: Behavior, - displayName$: Behavior, - mxcAvatarUrl$: Behavior, - public readonly local: boolean, - ) { - super( - scope, - id, - userId, - rtcBackendIdentity, - participant$, - encryptionSystem, - Track.Source.ScreenShareAudio, - Track.Source.ScreenShare, - livekitRoom$, - focusUrl$, - displayName$, - mxcAvatarUrl$, - ); - } -} diff --git a/src/state/ScreenShare.ts b/src/state/ScreenShare.ts deleted file mode 100644 index e4f5de1f..00000000 --- a/src/state/ScreenShare.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ -import { of } from "rxjs"; -import { - type LocalParticipant, - type RemoteParticipant, - type Room as LivekitRoom, -} from "livekit-client"; - -import { type ObservableScope } from "./ObservableScope.ts"; -import { ScreenShareViewModel } from "./MediaViewModel.ts"; -import type { EncryptionSystem } from "../e2ee/sharedKeyManagement.ts"; -import type { Behavior } from "./Behavior.ts"; - -/** - * A screen share media item to be presented in a tile. This is a thin wrapper - * around ScreenShareViewModel which essentially just establishes an - * ObservableScope for behaviors that the view model depends on. - */ -export class ScreenShare { - public readonly vm: ScreenShareViewModel; - - public constructor( - private readonly scope: ObservableScope, - id: string, - userId: string, - rtcBackendIdentity: string, - participant: LocalParticipant | RemoteParticipant, - encryptionSystem: EncryptionSystem, - livekitRoom$: Behavior, - focusUrl$: Behavior, - pretendToBeDisconnected$: Behavior, - displayName$: Behavior, - mxcAvatarUrl$: Behavior, - ) { - this.vm = new ScreenShareViewModel( - this.scope, - id, - userId, - rtcBackendIdentity, - of(participant), - encryptionSystem, - livekitRoom$, - focusUrl$, - pretendToBeDisconnected$, - displayName$, - mxcAvatarUrl$, - participant.isLocal, - ); - } -} diff --git a/src/state/TileStore.ts b/src/state/TileStore.ts index 7b95bd8e..a954eb4e 100644 --- a/src/state/TileStore.ts +++ b/src/state/TileStore.ts @@ -8,10 +8,11 @@ Please see LICENSE in the repository root for full details. import { BehaviorSubject } from "rxjs"; import { logger } from "matrix-js-sdk/lib/logger"; -import { type MediaViewModel, type UserMediaViewModel } from "./MediaViewModel"; import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel"; import { fillGaps } from "../utils/iter"; import { debugTileLayout } from "../settings/settings"; +import { type MediaViewModel } from "./media/MediaViewModel"; +import { type UserMediaViewModel } from "./media/UserMediaViewModel"; function debugEntries(entries: GridTileData[]): string[] { return entries.map((e) => e.media.displayName$.value); diff --git a/src/state/TileViewModel.ts b/src/state/TileViewModel.ts index a645a0d1..8b13c685 100644 --- a/src/state/TileViewModel.ts +++ b/src/state/TileViewModel.ts @@ -5,8 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type MediaViewModel, type UserMediaViewModel } from "./MediaViewModel"; import { type Behavior } from "./Behavior"; +import { type MediaViewModel } from "./media/MediaViewModel"; +import { type UserMediaViewModel } from "./media/UserMediaViewModel"; let nextId = 0; function createId(): string { diff --git a/src/state/UserMedia.ts b/src/state/UserMedia.ts deleted file mode 100644 index 74d24e2f..00000000 --- a/src/state/UserMedia.ts +++ /dev/null @@ -1,210 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { combineLatest, map, type Observable, of, switchMap } from "rxjs"; -import { - type LocalParticipant, - ParticipantEvent, - type RemoteParticipant, - type Room as LivekitRoom, -} from "livekit-client"; -import { observeParticipantEvents } from "@livekit/components-core"; - -import { type ObservableScope } from "./ObservableScope.ts"; -import { - LocalUserMediaViewModel, - RemoteUserMediaViewModel, - type UserMediaViewModel, -} from "./MediaViewModel.ts"; -import type { Behavior } from "./Behavior.ts"; -import type { EncryptionSystem } from "../e2ee/sharedKeyManagement.ts"; -import type { MediaDevices } from "./MediaDevices.ts"; -import type { ReactionOption } from "../reactions"; -import { observeSpeaker$ } from "./observeSpeaker.ts"; -import { generateItems } from "../utils/observable.ts"; -import { ScreenShare } from "./ScreenShare.ts"; -import { type TaggedParticipant } from "./CallViewModel/remoteMembers/MatrixLivekitMembers.ts"; - -/** - * Sorting bins defining the order in which media tiles appear in the layout. - */ -enum SortingBin { - /** - * Yourself, when the "always show self" option is on. - */ - SelfAlwaysShown, - /** - * Participants that are sharing their screen. - */ - Presenters, - /** - * Participants that have been speaking recently. - */ - Speakers, - /** - * Participants that have their hand raised. - */ - HandRaised, - /** - * Participants with video. - */ - Video, - /** - * Participants not sharing any video. - */ - NoVideo, - /** - * Yourself, when the "always show self" option is off. - */ - SelfNotAlwaysShown, -} - -/** - * A user media item to be presented in a tile. This is a thin wrapper around - * UserMediaViewModel which additionally determines the media item's sorting bin - * for inclusion in the call layout and tracks associated screen shares. - */ -export class UserMedia { - public readonly vm: UserMediaViewModel = - this.participant.type === "local" - ? new LocalUserMediaViewModel( - this.scope, - this.id, - this.userId, - this.rtcBackendIdentity, - this.participant.value$, - this.encryptionSystem, - this.livekitRoom$, - this.focusUrl$, - this.mediaDevices, - this.displayName$, - this.mxcAvatarUrl$, - this.scope.behavior(this.handRaised$), - this.scope.behavior(this.reaction$), - ) - : new RemoteUserMediaViewModel( - this.scope, - this.id, - this.userId, - this.rtcBackendIdentity, - this.participant.value$, - this.encryptionSystem, - this.livekitRoom$, - this.focusUrl$, - this.pretendToBeDisconnected$, - this.displayName$, - this.mxcAvatarUrl$, - this.scope.behavior(this.handRaised$), - this.scope.behavior(this.reaction$), - ); - - private readonly speaker$ = this.scope.behavior( - observeSpeaker$(this.vm.speaking$), - ); - - // TypeScript needs this widening of the type to happen in a separate statement - private readonly participant$: Behavior< - LocalParticipant | RemoteParticipant | null - > = this.participant.value$; - - /** - * All screen share media associated with this user media. - */ - public readonly screenShares$ = this.scope.behavior( - this.participant$.pipe( - switchMap((p) => - p === null - ? of([]) - : observeParticipantEvents( - p, - ParticipantEvent.TrackPublished, - ParticipantEvent.TrackUnpublished, - ParticipantEvent.LocalTrackPublished, - ParticipantEvent.LocalTrackUnpublished, - ).pipe( - // Technically more than one screen share might be possible... our - // MediaViewModels don't support it though since they look for a unique - // track for the given source. So generateItems here is a bit overkill. - generateItems( - `${this.id} screenShares$`, - function* (p) { - if (p.isScreenShareEnabled) - yield { - keys: ["screen-share"], - data: undefined, - }; - }, - (scope, _data$, key) => - new ScreenShare( - scope, - `${this.id}:${key}`, - this.userId, - this.rtcBackendIdentity, - p, - this.encryptionSystem, - this.livekitRoom$, - this.focusUrl$, - this.pretendToBeDisconnected$, - this.displayName$, - this.mxcAvatarUrl$, - ), - ), - ), - ), - ), - ); - - private readonly presenter$ = this.scope.behavior( - this.screenShares$.pipe(map((screenShares) => screenShares.length > 0)), - ); - - /** - * Which sorting bin the media item should be placed in. - */ - // This is exposed here rather than by UserMediaViewModel because it's only - // relevant to the layout algorithms; the MediaView component should be - // ignorant of this value. - public readonly bin$ = combineLatest( - [ - this.speaker$, - this.presenter$, - this.vm.videoEnabled$, - this.vm.handRaised$, - this.vm instanceof LocalUserMediaViewModel - ? this.vm.alwaysShow$ - : of(false), - ], - (speaker, presenter, video, handRaised, alwaysShow) => { - if (this.vm.local) - return alwaysShow - ? SortingBin.SelfAlwaysShown - : SortingBin.SelfNotAlwaysShown; - else if (presenter) return SortingBin.Presenters; - else if (speaker) return SortingBin.Speakers; - else if (handRaised) return SortingBin.HandRaised; - else if (video) return SortingBin.Video; - else return SortingBin.NoVideo; - }, - ); - - public constructor( - private readonly scope: ObservableScope, - public readonly id: string, - private readonly userId: string, - private readonly rtcBackendIdentity: string, - private readonly participant: TaggedParticipant, - private readonly encryptionSystem: EncryptionSystem, - private readonly livekitRoom$: Behavior, - private readonly focusUrl$: Behavior, - private readonly mediaDevices: MediaDevices, - private readonly pretendToBeDisconnected$: Behavior, - private readonly displayName$: Behavior, - private readonly mxcAvatarUrl$: Behavior, - private readonly handRaised$: Observable, - private readonly reaction$: Observable, - ) {} -} diff --git a/src/state/VolumeControls.ts b/src/state/VolumeControls.ts new file mode 100644 index 00000000..beb7ae00 --- /dev/null +++ b/src/state/VolumeControls.ts @@ -0,0 +1,101 @@ +/* +Copyright 2026 Element Software Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { combineLatest, map, merge, of, Subject, switchMap } from "rxjs"; + +import { type Behavior } from "./Behavior"; +import { type ObservableScope } from "./ObservableScope"; +import { accumulate } from "../utils/observable"; + +/** + * Controls for audio playback volume. + */ +export interface VolumeControls { + /** + * The volume to which the audio is set, as a scalar multiplier. + */ + playbackVolume$: Behavior; + /** + * Whether playback of this audio is disabled. + */ + playbackMuted$: Behavior; + togglePlaybackMuted: () => void; + adjustPlaybackVolume: (value: number) => void; + commitPlaybackVolume: () => void; +} + +interface VolumeControlsInputs { + pretendToBeDisconnected$: Behavior; + /** + * The callback to run to notify the module performing audio playback of the + * requested volume. + */ + sink$: Behavior<(volume: number) => void>; +} + +/** + * Creates a set of controls for audio playback volume and syncs this with the + * audio playback module for the duration of the scope. + */ +export function createVolumeControls( + scope: ObservableScope, + { pretendToBeDisconnected$, sink$ }: VolumeControlsInputs, +): VolumeControls { + const toggleMuted$ = new Subject<"toggle mute">(); + const adjustVolume$ = new Subject(); + const commitVolume$ = new Subject<"commit">(); + + const playbackVolume$ = scope.behavior( + merge(toggleMuted$, adjustVolume$, commitVolume$).pipe( + accumulate({ volume: 1, committedVolume: 1 }, (state, event) => { + switch (event) { + case "toggle mute": + return { + ...state, + volume: state.volume === 0 ? state.committedVolume : 0, + }; + case "commit": + // Dragging the slider to zero should have the same effect as + // muting: keep the original committed volume, as if it were never + // dragged + return { + ...state, + committedVolume: + state.volume === 0 ? state.committedVolume : state.volume, + }; + default: + // Volume adjustment + return { ...state, volume: event }; + } + }), + map(({ volume }) => volume), + ), + ); + + // Sync the requested volume with the audio playback module + combineLatest([ + sink$, + // The playback volume, taking into account whether we're supposed to + // pretend that the audio stream is disconnected (since we don't necessarily + // want that to modify the UI state). + pretendToBeDisconnected$.pipe( + switchMap((disconnected) => (disconnected ? of(0) : playbackVolume$)), + ), + ]) + .pipe(scope.bind()) + .subscribe(([sink, volume]) => sink(volume)); + + return { + playbackVolume$, + playbackMuted$: scope.behavior( + playbackVolume$.pipe(map((volume) => volume === 0)), + ), + togglePlaybackMuted: () => toggleMuted$.next("toggle mute"), + adjustPlaybackVolume: (value: number) => adjustVolume$.next(value), + commitPlaybackVolume: () => commitVolume$.next("commit"), + }; +} diff --git a/src/state/layout-types.ts b/src/state/layout-types.ts index 3796715c..33796f66 100644 --- a/src/state/layout-types.ts +++ b/src/state/layout-types.ts @@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ +import { type LocalUserMediaViewModel } from "./media/LocalUserMediaViewModel.ts"; +import { type MediaViewModel } from "./media/MediaViewModel.ts"; +import { type RemoteUserMediaViewModel } from "./media/RemoteUserMediaViewModel.ts"; +import { type UserMediaViewModel } from "./media/UserMediaViewModel.ts"; import { type GridTileViewModel, type SpotlightTileViewModel, } from "./TileViewModel.ts"; -import { - type MediaViewModel, - type UserMediaViewModel, -} from "./MediaViewModel.ts"; export interface GridLayoutMedia { type: "grid"; @@ -40,8 +40,8 @@ export interface SpotlightExpandedLayoutMedia { export interface OneOnOneLayoutMedia { type: "one-on-one"; - local: UserMediaViewModel; - remote: UserMediaViewModel; + local: LocalUserMediaViewModel; + remote: RemoteUserMediaViewModel; } export interface PipLayoutMedia { diff --git a/src/state/media/LocalScreenShareViewModel.ts b/src/state/media/LocalScreenShareViewModel.ts new file mode 100644 index 00000000..b31739d9 --- /dev/null +++ b/src/state/media/LocalScreenShareViewModel.ts @@ -0,0 +1,32 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { type LocalParticipant } from "livekit-client"; + +import { type Behavior } from "../Behavior"; +import { + type BaseScreenShareInputs, + type BaseScreenShareViewModel, + createBaseScreenShare, +} from "./ScreenShareViewModel"; +import { type ObservableScope } from "../ObservableScope"; + +export interface LocalScreenShareViewModel extends BaseScreenShareViewModel { + local: true; +} + +export interface LocalScreenShareInputs extends BaseScreenShareInputs { + participant$: Behavior; +} + +export function createLocalScreenShare( + scope: ObservableScope, + inputs: LocalScreenShareInputs, +): LocalScreenShareViewModel { + return { ...createBaseScreenShare(scope, inputs), local: true }; +} diff --git a/src/state/media/LocalUserMediaViewModel.ts b/src/state/media/LocalUserMediaViewModel.ts new file mode 100644 index 00000000..fd21428b --- /dev/null +++ b/src/state/media/LocalUserMediaViewModel.ts @@ -0,0 +1,137 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { + facingModeFromLocalTrack, + type LocalParticipant, + LocalVideoTrack, + TrackEvent, +} from "livekit-client"; +import { + fromEvent, + map, + merge, + type Observable, + of, + startWith, + switchMap, +} from "rxjs"; +import { logger } from "matrix-js-sdk/lib/logger"; + +import { type Behavior } from "../Behavior"; +import { + type BaseUserMediaInputs, + type BaseUserMediaViewModel, + createBaseUserMedia, +} from "./UserMediaViewModel"; +import { type ObservableScope } from "../ObservableScope"; +import { alwaysShowSelf } from "../../settings/settings"; +import { platform } from "../../Platform"; +import { type MediaDevices } from "../MediaDevices"; + +export interface LocalUserMediaViewModel extends BaseUserMediaViewModel { + local: true; + /** + * Whether the video should be mirrored. + */ + mirror$: Behavior; + /** + * Whether to show this tile in a highly visible location near the start of + * the grid. + */ + alwaysShow$: Behavior; + setAlwaysShow: (value: boolean) => void; + switchCamera$: Behavior<(() => void) | null>; +} + +export interface LocalUserMediaInputs extends Omit< + BaseUserMediaInputs, + "statsType" +> { + participant$: Behavior; + mediaDevices: MediaDevices; +} + +export function createLocalUserMedia( + scope: ObservableScope, + { mediaDevices, ...inputs }: LocalUserMediaInputs, +): LocalUserMediaViewModel { + const baseUserMedia = createBaseUserMedia(scope, { + ...inputs, + statsType: "outbound-rtp", + }); + + /** + * The local video track as an observable that emits whenever the track + * changes, the camera is switched, or the track is muted. + */ + const videoTrack$: Observable = + baseUserMedia.video$.pipe( + switchMap((v) => { + const track = v?.publication.track; + if (!(track instanceof LocalVideoTrack)) return of(null); + return merge( + // Watch for track restarts because they indicate a camera switch. + // This event is also emitted when unmuting the track object. + fromEvent(track, TrackEvent.Restarted).pipe( + startWith(null), + map(() => track), + ), + // When the track object is muted, reset it to null. + fromEvent(track, TrackEvent.Muted).pipe(map(() => null)), + ); + }), + ); + + return { + ...baseUserMedia, + local: true, + mirror$: scope.behavior( + videoTrack$.pipe( + // Mirror only front-facing cameras (those that face the user) + map( + (track) => + track !== null && + facingModeFromLocalTrack(track).facingMode === "user", + ), + ), + ), + alwaysShow$: alwaysShowSelf.value$, + setAlwaysShow: alwaysShowSelf.setValue, + switchCamera$: scope.behavior( + platform === "desktop" + ? of(null) + : videoTrack$.pipe( + map((track) => { + if (track === null) return null; + const facingMode = facingModeFromLocalTrack(track).facingMode; + // If the camera isn't front or back-facing, don't provide a switch + // camera shortcut at all + if (facingMode !== "user" && facingMode !== "environment") + return null; + // Restart the track with a camera facing the opposite direction + return (): void => + void track + .restartTrack({ + facingMode: facingMode === "user" ? "environment" : "user", + }) + .then(() => { + // Inform the MediaDevices which camera was chosen + const deviceId = + track.mediaStreamTrack.getSettings().deviceId; + if (deviceId !== undefined) + mediaDevices.videoInput.select(deviceId); + }) + .catch((e) => + logger.error("Failed to switch camera", facingMode, e), + ); + }), + ), + ), + }; +} diff --git a/src/state/media/MediaItem.ts b/src/state/media/MediaItem.ts new file mode 100644 index 00000000..6cd80045 --- /dev/null +++ b/src/state/media/MediaItem.ts @@ -0,0 +1,198 @@ +/* +Copyright 2025-2026 Element Software Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { combineLatest, map, of, switchMap } from "rxjs"; +import { + type LocalParticipant, + ParticipantEvent, + type RemoteParticipant, +} from "livekit-client"; +import { observeParticipantEvents } from "@livekit/components-core"; + +import { type ObservableScope } from "../ObservableScope.ts"; +import type { Behavior } from "../Behavior.ts"; +import type { MediaDevices } from "../MediaDevices.ts"; +import { observeSpeaker$ } from "./observeSpeaker.ts"; +import { generateItems } from "../../utils/observable.ts"; +import { type TaggedParticipant } from "../CallViewModel/remoteMembers/MatrixLivekitMembers.ts"; +import { type UserMediaViewModel } from "./UserMediaViewModel.ts"; +import { type ScreenShareViewModel } from "./ScreenShareViewModel.ts"; +import { + createLocalUserMedia, + type LocalUserMediaInputs, +} from "./LocalUserMediaViewModel.ts"; +import { + createRemoteUserMedia, + type RemoteUserMediaInputs, +} from "./RemoteUserMediaViewModel.ts"; +import { createLocalScreenShare } from "./LocalScreenShareViewModel.ts"; +import { createRemoteScreenShare } from "./RemoteScreenShareViewModel.ts"; + +/** + * Sorting bins defining the order in which media tiles appear in the layout. + */ +enum SortingBin { + /** + * Yourself, when the "always show self" option is on. + */ + SelfAlwaysShown, + /** + * Participants that are sharing their screen. + */ + Presenters, + /** + * Participants that have been speaking recently. + */ + Speakers, + /** + * Participants that have their hand raised. + */ + HandRaised, + /** + * Participants with video. + */ + Video, + /** + * Participants not sharing any video. + */ + NoVideo, + /** + * Yourself, when the "always show self" option is off. + */ + SelfNotAlwaysShown, +} + +/** + * A user media item to be presented in a tile. This is a thin wrapper around + * UserMediaViewModel which additionally carries data relevant to the tile + * layout algorithms (data which the MediaView component should be ignorant of). + */ +export type WrappedUserMediaViewModel = UserMediaViewModel & { + /** + * All screen share media associated with this user media. + */ + screenShares$: Behavior; + /** + * Which sorting bin the media item should be placed in. + */ + bin$: Behavior; +}; + +interface WrappedUserMediaInputs extends Omit< + LocalUserMediaInputs & RemoteUserMediaInputs, + "participant$" +> { + participant: TaggedParticipant; + mediaDevices: MediaDevices; + pretendToBeDisconnected$: Behavior; +} + +export function createWrappedUserMedia( + scope: ObservableScope, + { + participant, + mediaDevices, + pretendToBeDisconnected$, + ...inputs + }: WrappedUserMediaInputs, +): WrappedUserMediaViewModel { + const userMedia = + participant.type === "local" + ? createLocalUserMedia(scope, { + participant$: participant.value$, + mediaDevices, + ...inputs, + }) + : createRemoteUserMedia(scope, { + participant$: participant.value$, + pretendToBeDisconnected$, + ...inputs, + }); + + // TypeScript needs this widening of the type to happen in a separate statement + const participant$: Behavior = + participant.value$; + + const screenShares$ = scope.behavior( + participant$.pipe( + switchMap((p) => + p === null + ? of([]) + : observeParticipantEvents( + p, + ParticipantEvent.TrackPublished, + ParticipantEvent.TrackUnpublished, + ParticipantEvent.LocalTrackPublished, + ParticipantEvent.LocalTrackUnpublished, + ).pipe( + // Technically more than one screen share might be possible... our + // MediaViewModels don't support it though since they look for a unique + // track for the given source. So generateItems here is a bit overkill. + generateItems( + `${inputs.id} screenShares$`, + function* (p) { + if (p.isScreenShareEnabled) + yield { + keys: ["screen-share"], + data: undefined, + }; + }, + (scope, _data$, key) => { + const id = `${inputs.id}:${key}`; + return participant.type === "local" + ? createLocalScreenShare(scope, { + ...inputs, + id, + participant$: participant.value$, + }) + : createRemoteScreenShare(scope, { + ...inputs, + id, + participant$: participant.value$, + pretendToBeDisconnected$, + }); + }, + ), + ), + ), + ), + ); + + const speaker$ = scope.behavior(observeSpeaker$(userMedia.speaking$)); + const presenter$ = scope.behavior( + screenShares$.pipe(map((screenShares) => screenShares.length > 0)), + ); + + return { + ...userMedia, + screenShares$, + bin$: scope.behavior( + combineLatest( + [ + speaker$, + presenter$, + userMedia.videoEnabled$, + userMedia.handRaised$, + userMedia.local ? userMedia.alwaysShow$ : of(null), + ], + (speaker, presenter, video, handRaised, alwaysShow) => { + if (alwaysShow !== null) + return alwaysShow + ? SortingBin.SelfAlwaysShown + : SortingBin.SelfNotAlwaysShown; + else if (presenter) return SortingBin.Presenters; + else if (speaker) return SortingBin.Speakers; + else if (handRaised) return SortingBin.HandRaised; + else if (video) return SortingBin.Video; + else return SortingBin.NoVideo; + }, + ), + ), + }; +} + +export type MediaItem = WrappedUserMediaViewModel | ScreenShareViewModel; diff --git a/src/state/MediaViewModel.test.ts b/src/state/media/MediaViewModel.test.ts similarity index 85% rename from src/state/MediaViewModel.test.ts rename to src/state/media/MediaViewModel.test.ts index a7bbb571..4cb137db 100644 --- a/src/state/MediaViewModel.test.ts +++ b/src/state/media/MediaViewModel.test.ts @@ -17,13 +17,12 @@ import { mockLocalParticipant, mockMediaDevices, mockRtcMembership, - createLocalMedia, - createRemoteMedia, + mockLocalMedia, + mockRemoteMedia, withTestScheduler, mockRemoteParticipant, -} from "../utils/test"; -import { getValue } from "../utils/observable"; -import { constant } from "./Behavior"; +} from "../../utils/test"; +import { constant } from "../Behavior"; global.MediaStreamTrack = class {} as unknown as { new (): MediaStreamTrack; @@ -35,7 +34,7 @@ global.MediaStream = class {} as unknown as { }; const platformMock = vi.hoisted(() => vi.fn(() => "desktop")); -vi.mock("../Platform", () => ({ +vi.mock("../../Platform", () => ({ get platform(): string { return platformMock(); }, @@ -45,7 +44,7 @@ const rtcMembership = mockRtcMembership("@alice:example.org", "AAAA"); test("control a participant's volume", () => { const setVolumeSpy = vi.fn(); - const vm = createRemoteMedia( + const vm = mockRemoteMedia( rtcMembership, {}, mockRemoteParticipant({ setVolume: setVolumeSpy }), @@ -54,33 +53,33 @@ test("control a participant's volume", () => { schedule("-ab---c---d|", { a() { // Try muting by toggling - vm.toggleLocallyMuted(); + vm.togglePlaybackMuted(); expect(setVolumeSpy).toHaveBeenLastCalledWith(0); }, b() { // Try unmuting by dragging the slider back up - vm.setLocalVolume(0.6); - vm.setLocalVolume(0.8); - vm.commitLocalVolume(); + vm.adjustPlaybackVolume(0.6); + vm.adjustPlaybackVolume(0.8); + vm.commitPlaybackVolume(); expect(setVolumeSpy).toHaveBeenCalledWith(0.6); expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8); }, c() { // Try muting by dragging the slider back down - vm.setLocalVolume(0.2); - vm.setLocalVolume(0); - vm.commitLocalVolume(); + vm.adjustPlaybackVolume(0.2); + vm.adjustPlaybackVolume(0); + vm.commitPlaybackVolume(); expect(setVolumeSpy).toHaveBeenCalledWith(0.2); expect(setVolumeSpy).toHaveBeenLastCalledWith(0); }, d() { // Try unmuting by toggling - vm.toggleLocallyMuted(); + vm.togglePlaybackMuted(); // The volume should return to the last non-zero committed volume expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8); }, }); - expectObservable(vm.localVolume$).toBe("ab(cd)(ef)g", { + expectObservable(vm.playbackVolume$).toBe("ab(cd)(ef)g", { a: 1, b: 0, c: 0.6, @@ -93,7 +92,7 @@ test("control a participant's volume", () => { }); test("local media remembers whether it should always be shown", () => { - const vm1 = createLocalMedia( + const vm1 = mockLocalMedia( rtcMembership, {}, mockLocalParticipant({}), @@ -105,7 +104,7 @@ test("local media remembers whether it should always be shown", () => { }); // Next local media should start out *not* always shown - const vm2 = createLocalMedia( + const vm2 = mockLocalMedia( rtcMembership, {}, mockLocalParticipant({}), @@ -151,7 +150,7 @@ test("switch cameras", async () => { const selectVideoInput = vi.fn(); - const vm = createLocalMedia( + const vm = mockLocalMedia( rtcMembership, {}, mockLocalParticipant({ @@ -169,7 +168,7 @@ test("switch cameras", async () => { ); // Switch to back camera - getValue(vm.switchCamera$)!(); + vm.switchCamera$.value!(); expect(restartTrack).toHaveBeenCalledExactlyOnceWith({ facingMode: "environment", }); @@ -180,7 +179,7 @@ test("switch cameras", async () => { expect(deviceId).toBe("back camera"); // Switch to front camera - getValue(vm.switchCamera$)!(); + vm.switchCamera$.value!(); expect(restartTrack).toHaveBeenCalledTimes(2); expect(restartTrack).toHaveBeenLastCalledWith({ facingMode: "user" }); await waitFor(() => { @@ -191,17 +190,17 @@ test("switch cameras", async () => { }); test("remote media is in waiting state when participant has not yet connected", () => { - const vm = createRemoteMedia(rtcMembership, {}, null); // null participant + const vm = mockRemoteMedia(rtcMembership, {}, null); // null participant expect(vm.waitingForMedia$.value).toBe(true); }); test("remote media is not in waiting state when participant is connected", () => { - const vm = createRemoteMedia(rtcMembership, {}, mockRemoteParticipant({})); + const vm = mockRemoteMedia(rtcMembership, {}, mockRemoteParticipant({})); expect(vm.waitingForMedia$.value).toBe(false); }); test("remote media is not in waiting state when participant is connected with no publications", () => { - const vm = createRemoteMedia( + const vm = mockRemoteMedia( rtcMembership, {}, mockRemoteParticipant({ @@ -213,7 +212,7 @@ test("remote media is not in waiting state when participant is connected with no }); test("remote media is not in waiting state when user does not intend to publish anywhere", () => { - const vm = createRemoteMedia( + const vm = mockRemoteMedia( rtcMembership, {}, mockRemoteParticipant({}), diff --git a/src/state/media/MediaViewModel.ts b/src/state/media/MediaViewModel.ts new file mode 100644 index 00000000..bdc4875b --- /dev/null +++ b/src/state/media/MediaViewModel.ts @@ -0,0 +1,44 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { type Behavior } from "../Behavior"; +import { type ScreenShareViewModel } from "./ScreenShareViewModel"; +import { type UserMediaViewModel } from "./UserMediaViewModel"; + +/** + * A participant's media. + */ +export type MediaViewModel = UserMediaViewModel | ScreenShareViewModel; + +/** + * Properties which are common to all MediaViewModels. + */ +export interface BaseMediaViewModel { + /** + * An opaque identifier for this media. + */ + id: string; + /** + * The Matrix user to which this media belongs. + */ + userId: string; + displayName$: Behavior; + mxcAvatarUrl$: Behavior; +} + +export type BaseMediaInputs = BaseMediaViewModel; + +// All this function does is strip out superfluous data from the input object +export function createBaseMedia({ + id, + userId, + displayName$, + mxcAvatarUrl$, +}: BaseMediaInputs): BaseMediaViewModel { + return { id, userId, displayName$, mxcAvatarUrl$ }; +} diff --git a/src/state/media/MemberMediaViewModel.ts b/src/state/media/MemberMediaViewModel.ts new file mode 100644 index 00000000..e7f57b59 --- /dev/null +++ b/src/state/media/MemberMediaViewModel.ts @@ -0,0 +1,272 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { + type Room as LivekitRoom, + RoomEvent as LivekitRoomEvent, + type Participant, + type Track, +} from "livekit-client"; +import { + type AudioSource, + roomEventSelector, + type TrackReference, + type VideoSource, +} from "@livekit/components-core"; +import { type LocalParticipant, type RemoteParticipant } from "livekit-client"; +import { + combineLatest, + distinctUntilChanged, + filter, + map, + type Observable, + of, + startWith, + switchMap, + throttleTime, +} from "rxjs"; + +import { type Behavior } from "../Behavior"; +import { type BaseMediaViewModel, createBaseMedia } from "./MediaViewModel"; +import { type EncryptionSystem } from "../../e2ee/sharedKeyManagement"; +import { type ObservableScope } from "../ObservableScope"; +import { observeTrackReference$ } from "../observeTrackReference"; +import { E2eeType } from "../../e2ee/e2eeType"; +import { observeInboundRtpStreamStats$ } from "./observeRtpStreamStats"; + +// TODO: Encryption status is kinda broken and thus unused right now. Remove? +export enum EncryptionStatus { + Connecting, + Okay, + KeyMissing, + KeyInvalid, + PasswordInvalid, +} + +/** + * Media belonging to an active member of the RTC session. + */ +export interface MemberMediaViewModel extends BaseMediaViewModel { + /** + * The LiveKit video track for this media. + */ + video$: Behavior; + /** + * The URL of the LiveKit focus on which this member should be publishing. + * Exposed for debugging. + */ + focusUrl$: Behavior; + /** + * Whether there should be a warning that this media is unencrypted. + */ + unencryptedWarning$: Behavior; + encryptionStatus$: Behavior; +} + +export interface MemberMediaInputs extends BaseMediaViewModel { + participant$: Behavior; + livekitRoom$: Behavior; + audioSource: AudioSource; + videoSource: VideoSource; + focusUrl$: Behavior; + encryptionSystem: EncryptionSystem; +} + +export function createMemberMedia( + scope: ObservableScope, + { + participant$, + livekitRoom$, + audioSource, + videoSource, + focusUrl$, + encryptionSystem, + ...inputs + }: MemberMediaInputs, +): MemberMediaViewModel { + const trackBehavior$ = ( + source: Track.Source, + ): Behavior => + scope.behavior( + participant$.pipe( + switchMap((p) => + !p ? of(undefined) : observeTrackReference$(p, source), + ), + ), + ); + + const audio$ = trackBehavior$(audioSource); + const video$ = trackBehavior$(videoSource); + + return { + ...createBaseMedia(inputs), + video$, + focusUrl$, + unencryptedWarning$: scope.behavior( + combineLatest( + [audio$, video$], + (a, v) => + encryptionSystem.kind !== E2eeType.NONE && + (a?.publication.isEncrypted === false || + v?.publication.isEncrypted === false), + ), + ), + encryptionStatus$: scope.behavior( + 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), + ); + } + }), + ), + ), + }; +} + +function encryptionErrorObservable$( + room$: Behavior, + participant: Participant, + encryptionSystem: EncryptionSystem, + criteria: string, +): Observable { + return room$.pipe( + switchMap((room) => { + if (room === undefined) return of(false); + return roomEventSelector(room, LivekitRoomEvent.EncryptionError).pipe( + map((e) => { + const [err] = e; + if (encryptionSystem.kind === E2eeType.PER_PARTICIPANT) { + return ( + // Ideally we would pull the participant identity from the field on the error. + // However, it gets lost in the serialization process between workers. + // So, instead we do a string match + (err?.message.includes(participant.identity) && + err?.message.includes(criteria)) ?? + false + ); + } else if (encryptionSystem.kind === E2eeType.SHARED_KEY) { + return !!err?.message.includes(criteria); + } + + return false; + }), + ); + }), + distinctUntilChanged(), + throttleTime(1000), // Throttle to avoid spamming the UI + startWith(false), + ); +} + +function observeRemoteTrackReceivingOkay$( + participant: Participant, + source: Track.Source, +): Observable { + let lastStats: { + framesDecoded: number | undefined; + framesDropped: number | undefined; + framesReceived: number | undefined; + } = { + framesDecoded: undefined, + framesDropped: undefined, + framesReceived: undefined, + }; + + return observeInboundRtpStreamStats$(participant, source).pipe( + map((stats) => { + if (!stats) return undefined; + const { framesDecoded, framesDropped, framesReceived } = stats; + return { + framesDecoded, + framesDropped, + framesReceived, + }; + }), + filter((newStats) => !!newStats), + map((newStats): boolean | undefined => { + const oldStats = lastStats; + lastStats = newStats; + if ( + typeof newStats.framesReceived === "number" && + typeof oldStats.framesReceived === "number" && + typeof newStats.framesDecoded === "number" && + typeof oldStats.framesDecoded === "number" + ) { + const framesReceivedDelta = + newStats.framesReceived - oldStats.framesReceived; + const framesDecodedDelta = + newStats.framesDecoded - oldStats.framesDecoded; + + // if we received >0 frames and managed to decode >0 frames then we treat that as success + + if (framesReceivedDelta > 0) { + return framesDecodedDelta > 0; + } + } + + // no change + return undefined; + }), + filter((x) => typeof x === "boolean"), + startWith(undefined), + ); +} diff --git a/src/state/media/RemoteScreenShareViewModel.ts b/src/state/media/RemoteScreenShareViewModel.ts new file mode 100644 index 00000000..eff6d9c1 --- /dev/null +++ b/src/state/media/RemoteScreenShareViewModel.ts @@ -0,0 +1,44 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { type RemoteParticipant } from "livekit-client"; +import { map } from "rxjs"; + +import { type Behavior } from "../Behavior"; +import { + type BaseScreenShareInputs, + type BaseScreenShareViewModel, + createBaseScreenShare, +} from "./ScreenShareViewModel"; +import { type ObservableScope } from "../ObservableScope"; + +export interface RemoteScreenShareViewModel extends BaseScreenShareViewModel { + local: false; + /** + * Whether this screen share's video should be displayed. + */ + videoEnabled$: Behavior; +} + +export interface RemoteScreenShareInputs extends BaseScreenShareInputs { + participant$: Behavior; + pretendToBeDisconnected$: Behavior; +} + +export function createRemoteScreenShare( + scope: ObservableScope, + { pretendToBeDisconnected$, ...inputs }: RemoteScreenShareInputs, +): RemoteScreenShareViewModel { + return { + ...createBaseScreenShare(scope, inputs), + local: false, + videoEnabled$: scope.behavior( + pretendToBeDisconnected$.pipe(map((disconnected) => !disconnected)), + ), + }; +} diff --git a/src/state/media/RemoteUserMediaViewModel.ts b/src/state/media/RemoteUserMediaViewModel.ts new file mode 100644 index 00000000..4307dea4 --- /dev/null +++ b/src/state/media/RemoteUserMediaViewModel.ts @@ -0,0 +1,82 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { type RemoteParticipant } from "livekit-client"; +import { combineLatest, map, of, switchMap } from "rxjs"; + +import { type Behavior } from "../Behavior"; +import { createVolumeControls, type VolumeControls } from "../VolumeControls"; +import { + type BaseUserMediaInputs, + type BaseUserMediaViewModel, + createBaseUserMedia, +} from "./UserMediaViewModel"; +import { type ObservableScope } from "../ObservableScope"; + +export interface RemoteUserMediaViewModel + extends BaseUserMediaViewModel, VolumeControls { + local: false; + /** + * Whether we are waiting for this user's LiveKit participant to exist. This + * could be because either we or the remote party are still connecting. + */ + waitingForMedia$: Behavior; +} + +export interface RemoteUserMediaInputs extends Omit< + BaseUserMediaInputs, + "statsType" +> { + participant$: Behavior; + pretendToBeDisconnected$: Behavior; +} + +export function createRemoteUserMedia( + scope: ObservableScope, + { pretendToBeDisconnected$, ...inputs }: RemoteUserMediaInputs, +): RemoteUserMediaViewModel { + const baseUserMedia = createBaseUserMedia(scope, { + ...inputs, + statsType: "inbound-rtp", + }); + + return { + ...baseUserMedia, + ...createVolumeControls(scope, { + pretendToBeDisconnected$, + sink$: scope.behavior( + inputs.participant$.pipe(map((p) => (volume) => p?.setVolume(volume))), + ), + }), + local: false, + speaking$: scope.behavior( + pretendToBeDisconnected$.pipe( + switchMap((disconnected) => + disconnected ? of(false) : baseUserMedia.speaking$, + ), + ), + ), + videoEnabled$: scope.behavior( + pretendToBeDisconnected$.pipe( + switchMap((disconnected) => + disconnected ? of(false) : baseUserMedia.videoEnabled$, + ), + ), + ), + waitingForMedia$: scope.behavior( + combineLatest( + [inputs.livekitRoom$, inputs.participant$], + (livekitRoom, participant) => + // If livekitRoom is undefined, the user is not attempting to publish on + // any transport and so we shouldn't expect a participant. (They might + // be a subscribe-only bot for example.) + livekitRoom !== undefined && participant === null, + ), + ), + }; +} diff --git a/src/state/media/ScreenShareViewModel.ts b/src/state/media/ScreenShareViewModel.ts new file mode 100644 index 00000000..36cd9440 --- /dev/null +++ b/src/state/media/ScreenShareViewModel.ts @@ -0,0 +1,51 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { Track } from "livekit-client"; + +import { type ObservableScope } from "../ObservableScope"; +import { type LocalScreenShareViewModel } from "./LocalScreenShareViewModel"; +import { + createMemberMedia, + type MemberMediaInputs, + type MemberMediaViewModel, +} from "./MemberMediaViewModel"; +import { type RemoteScreenShareViewModel } from "./RemoteScreenShareViewModel"; + +/** + * A participant's screen share media. + */ +export type ScreenShareViewModel = + | LocalScreenShareViewModel + | RemoteScreenShareViewModel; + +/** + * Properties which are common to all ScreenShareViewModels. + */ +export interface BaseScreenShareViewModel extends MemberMediaViewModel { + type: "screen share"; +} + +export type BaseScreenShareInputs = Omit< + MemberMediaInputs, + "audioSource" | "videoSource" +>; + +export function createBaseScreenShare( + scope: ObservableScope, + inputs: BaseScreenShareInputs, +): BaseScreenShareViewModel { + return { + ...createMemberMedia(scope, { + ...inputs, + audioSource: Track.Source.ScreenShareAudio, + videoSource: Track.Source.ScreenShare, + }), + type: "screen share", + }; +} diff --git a/src/state/media/UserMediaViewModel.ts b/src/state/media/UserMediaViewModel.ts new file mode 100644 index 00000000..1b9f6cbb --- /dev/null +++ b/src/state/media/UserMediaViewModel.ts @@ -0,0 +1,162 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { + BehaviorSubject, + combineLatest, + map, + type Observable, + of, + Subject, + switchMap, +} from "rxjs"; +import { + observeParticipantEvents, + observeParticipantMedia, +} from "@livekit/components-core"; +import { ParticipantEvent, Track } from "livekit-client"; + +import { type ReactionOption } from "../../reactions"; +import { type Behavior } from "../Behavior"; +import { type LocalUserMediaViewModel } from "./LocalUserMediaViewModel"; +import { + createMemberMedia, + type MemberMediaInputs, + type MemberMediaViewModel, +} from "./MemberMediaViewModel"; +import { type RemoteUserMediaViewModel } from "./RemoteUserMediaViewModel"; +import { type ObservableScope } from "../ObservableScope"; +import { showConnectionStats } from "../../settings/settings"; +import { observeRtpStreamStats$ } from "./observeRtpStreamStats"; +import { videoFit$, videoSizeFromParticipant$ } from "../../utils/videoFit.ts"; + +/** + * A participant's user media (i.e. their microphone and camera feed). + */ +export type UserMediaViewModel = + | LocalUserMediaViewModel + | RemoteUserMediaViewModel; + +export interface BaseUserMediaViewModel extends MemberMediaViewModel { + type: "user"; + speaking$: Behavior; + audioEnabled$: Behavior; + videoEnabled$: Behavior; + videoFit$: Behavior<"cover" | "contain">; + toggleCropVideo: () => void; + /** + * The expected identity of the LiveKit participant. Exposed for debugging. + */ + rtcBackendIdentity: string; + handRaised$: Behavior; + reaction$: Behavior; + audioStreamStats$: Observable< + RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined + >; + videoStreamStats$: Observable< + RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined + >; + /** + * Set the actual dimensions of the HTML element. + * This can be used to determine the best video fit (fit to frame / keep ratio). + * @param width - The actual width of the HTML element displaying the video. + * @param height - The actual height of the HTML element displaying the video. + */ + setActualDimensions: (width: number, height: number) => void; +} + +export interface BaseUserMediaInputs extends Omit< + MemberMediaInputs, + "audioSource" | "videoSource" +> { + rtcBackendIdentity: string; + handRaised$: Behavior; + reaction$: Behavior; + statsType: "inbound-rtp" | "outbound-rtp"; +} + +export function createBaseUserMedia( + scope: ObservableScope, + { + rtcBackendIdentity, + handRaised$, + reaction$, + statsType, + ...inputs + }: BaseUserMediaInputs, +): BaseUserMediaViewModel { + const { participant$ } = inputs; + const media$ = scope.behavior( + participant$.pipe( + switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)), + ), + ); + const toggleCropVideo$ = new Subject(); + + const actualSize$ = new BehaviorSubject< + { width: number; height: number } | undefined + >(undefined); + + return { + ...createMemberMedia(scope, { + ...inputs, + audioSource: Track.Source.Microphone, + videoSource: Track.Source.Camera, + }), + type: "user", + speaking$: scope.behavior( + participant$.pipe( + switchMap((p) => + p + ? observeParticipantEvents( + p, + ParticipantEvent.IsSpeakingChanged, + ).pipe(map((p) => p.isSpeaking)) + : of(false), + ), + ), + ), + audioEnabled$: scope.behavior( + media$.pipe(map((m) => m?.microphoneTrack?.isMuted === false)), + ), + videoEnabled$: scope.behavior( + media$.pipe(map((m) => m?.cameraTrack?.isMuted === false)), + ), + videoFit$: videoFit$( + scope, + videoSizeFromParticipant$(participant$), + actualSize$, + ), + toggleCropVideo: () => toggleCropVideo$.next(), + rtcBackendIdentity, + handRaised$, + reaction$, + audioStreamStats$: combineLatest([ + participant$, + showConnectionStats.value$, + ]).pipe( + switchMap(([p, showConnectionStats]) => { + // + if (!p || !showConnectionStats) return of(undefined); + return observeRtpStreamStats$(p, Track.Source.Microphone, statsType); + }), + ), + videoStreamStats$: combineLatest([ + participant$, + showConnectionStats.value$, + ]).pipe( + switchMap(([p, showConnectionStats]) => { + if (!p || !showConnectionStats) return of(undefined); + return observeRtpStreamStats$(p, Track.Source.Camera, statsType); + }), + ), + setActualDimensions: (width: number, height: number): void => { + actualSize$.next({ width, height }); + }, + }; +} diff --git a/src/state/media/observeRtpStreamStats.ts b/src/state/media/observeRtpStreamStats.ts new file mode 100644 index 00000000..63fb1a1b --- /dev/null +++ b/src/state/media/observeRtpStreamStats.ts @@ -0,0 +1,78 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { + LocalTrack, + type Participant, + RemoteTrack, + type Track, +} from "livekit-client"; +import { + combineLatest, + interval, + type Observable, + startWith, + switchMap, + map, +} from "rxjs"; + +import { observeTrackReference$ } from "../observeTrackReference"; + +export function observeRtpStreamStats$( + participant: Participant, + source: Track.Source, + type: "inbound-rtp" | "outbound-rtp", +): Observable< + RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined +> { + return combineLatest([ + observeTrackReference$(participant, source), + interval(1000).pipe(startWith(0)), + ]).pipe( + switchMap(async ([trackReference]) => { + const track = trackReference?.publication?.track; + if ( + !track || + !(track instanceof RemoteTrack || track instanceof LocalTrack) + ) { + return undefined; + } + const report = await track.getRTCStatsReport(); + if (!report) { + return undefined; + } + + for (const v of report.values()) { + if (v.type === type) { + return v; + } + } + + return undefined; + }), + startWith(undefined), + ); +} + +export function observeInboundRtpStreamStats$( + participant: Participant, + source: Track.Source, +): Observable { + return observeRtpStreamStats$(participant, source, "inbound-rtp").pipe( + map((x) => x as RTCInboundRtpStreamStats | undefined), + ); +} + +export function observeOutboundRtpStreamStats$( + participant: Participant, + source: Track.Source, +): Observable { + return observeRtpStreamStats$(participant, source, "outbound-rtp").pipe( + map((x) => x as RTCOutboundRtpStreamStats | undefined), + ); +} diff --git a/src/state/observeSpeaker.test.ts b/src/state/media/observeSpeaker.test.ts similarity index 98% rename from src/state/observeSpeaker.test.ts rename to src/state/media/observeSpeaker.test.ts index 224916d2..18622fb8 100644 --- a/src/state/observeSpeaker.test.ts +++ b/src/state/media/observeSpeaker.test.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { describe, test } from "vitest"; -import { withTestScheduler } from "../utils/test"; +import { withTestScheduler } from "../../utils/test"; import { observeSpeaker$ } from "./observeSpeaker"; const yesNo = { diff --git a/src/state/observeSpeaker.ts b/src/state/media/observeSpeaker.ts similarity index 100% rename from src/state/observeSpeaker.ts rename to src/state/media/observeSpeaker.ts diff --git a/src/state/observeTrackReference.ts b/src/state/observeTrackReference.ts new file mode 100644 index 00000000..8e295d05 --- /dev/null +++ b/src/state/observeTrackReference.ts @@ -0,0 +1,28 @@ +/* +Copyright 2023, 2024 New Vector Ltd. +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { + observeParticipantMedia, + type TrackReference, +} from "@livekit/components-core"; +import { type Participant, type Track } from "livekit-client"; +import { distinctUntilChanged, map, type Observable } from "rxjs"; + +/** + * Reactively reads a participant's track reference for a given media source. + */ +export function observeTrackReference$( + participant: Participant, + source: Track.Source, +): Observable { + return observeParticipantMedia(participant).pipe( + map(() => participant.getTrackPublication(source)), + distinctUntilChanged(), + map((publication) => publication && { participant, publication, source }), + ); +} diff --git a/src/tile/GridTile.test.tsx b/src/tile/GridTile.test.tsx index 060119ef..2c6e0d15 100644 --- a/src/tile/GridTile.test.tsx +++ b/src/tile/GridTile.test.tsx @@ -14,7 +14,7 @@ import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; import { GridTile } from "./GridTile"; import { mockRtcMembership, - createRemoteMedia, + mockRemoteMedia, mockRemoteParticipant, } from "../utils/test"; import { GridTileViewModel } from "../state/TileViewModel"; @@ -45,7 +45,7 @@ beforeAll(() => { }); test("GridTile is accessible", async () => { - const vm = createRemoteMedia( + const vm = mockRemoteMedia( mockRtcMembership("@alice:example.org", "AAAA"), { rawDisplayName: "Alice", diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index ad158db1..a9ae188b 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -40,11 +40,6 @@ import { useObservableEagerState } from "observable-hooks"; import useMeasure from "react-use-measure"; import styles from "./GridTile.module.css"; -import { - type UserMediaViewModel, - LocalUserMediaViewModel, - type RemoteUserMediaViewModel, -} from "../state/MediaViewModel"; import { Slider } from "../Slider"; import { MediaView } from "./MediaView"; import { useLatest } from "../useLatest"; @@ -52,6 +47,9 @@ import { type GridTileViewModel } from "../state/TileViewModel"; import { useMergedRefs } from "../useMergedRefs"; import { useReactionsSender } from "../reactions/useReactionsSender"; import { useBehavior } from "../useBehavior"; +import { type LocalUserMediaViewModel } from "../state/media/LocalUserMediaViewModel"; +import { type RemoteUserMediaViewModel } from "../state/media/RemoteUserMediaViewModel"; +import { type UserMediaViewModel } from "../state/media/UserMediaViewModel"; interface TileProps { ref?: Ref; @@ -69,7 +67,7 @@ interface TileProps { interface UserMediaTileProps extends TileProps { vm: UserMediaViewModel; mirror: boolean; - locallyMuted: boolean; + playbackMuted: boolean; waitingForMedia?: boolean; primaryButton?: ReactNode; menuStart?: ReactNode; @@ -80,7 +78,7 @@ const UserMediaTile: FC = ({ ref, vm, showSpeakingIndicators, - locallyMuted, + playbackMuted, waitingForMedia, primaryButton, menuStart, @@ -126,12 +124,12 @@ const UserMediaTile: FC = ({ } }, [bounds.width, bounds.height, vm]); - const AudioIcon = locallyMuted + const AudioIcon = playbackMuted ? VolumeOffSolidIcon : audioEnabled ? MicOnSolidIcon : MicOffSolidIcon; - const audioIconLabel = locallyMuted + const audioIconLabel = playbackMuted ? t("video_tile.muted_for_me") : audioEnabled ? t("microphone_on") @@ -173,7 +171,7 @@ const UserMediaTile: FC = ({ width={20} height={20} aria-label={audioIconLabel} - data-muted={locallyMuted || !audioEnabled} + data-muted={playbackMuted || !audioEnabled} className={styles.muteIcon} /> } @@ -252,7 +250,7 @@ const LocalUserMediaTile: FC = ({ = ({ }) => { const { t } = useTranslation(); const waitingForMedia = useBehavior(vm.waitingForMedia$); - const locallyMuted = useBehavior(vm.locallyMuted$); - const localVolume = useBehavior(vm.localVolume$); + const playbackMuted = useBehavior(vm.playbackMuted$); + const playbackVolume = useBehavior(vm.playbackVolume$); const onSelectMute = useCallback( (e: Event) => { e.preventDefault(); - vm.toggleLocallyMuted(); + vm.togglePlaybackMuted(); }, [vm], ); - const onChangeLocalVolume = useCallback( - (v: number) => vm.setLocalVolume(v), - [vm], - ); - const onCommitLocalVolume = useCallback(() => vm.commitLocalVolume(), [vm]); - const VolumeIcon = locallyMuted ? VolumeOffIcon : VolumeOnIcon; + const VolumeIcon = playbackMuted ? VolumeOffIcon : VolumeOnIcon; return ( {/* TODO: Figure out how to make this slider keyboard accessible */} @@ -339,9 +332,9 @@ const RemoteUserMediaTile: FC = ({ = ({ const displayName = useBehavior(media.displayName$); const mxcAvatarUrl = useBehavior(media.mxcAvatarUrl$); - if (media instanceof LocalUserMediaViewModel) { + if (media.local) { return ( { diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx index fadc9d2b..f912c069 100644 --- a/src/tile/MediaView.tsx +++ b/src/tile/MediaView.tsx @@ -16,10 +16,10 @@ import { ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/ico import styles from "./MediaView.module.css"; import { Avatar } from "../Avatar"; -import { type EncryptionStatus } from "../state/MediaViewModel"; +import { type EncryptionStatus } from "../state/media/MemberMediaViewModel"; import { RaisedHandIndicator } from "../reactions/RaisedHandIndicator"; import { - showConnectionStats, + showConnectionStats as showConnectionStatsSetting, showHandRaisedTimer, useSetting, } from "../settings/settings"; @@ -85,7 +85,7 @@ export const MediaView: FC = ({ }) => { const { t } = useTranslation(); const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer); - const [showConnectioStats] = useSetting(showConnectionStats); + const [showConnectionStats] = useSetting(showConnectionStatsSetting); const avatarSize = Math.round(Math.min(targetWidth, targetHeight) / 2); @@ -139,10 +139,10 @@ export const MediaView: FC = ({ {waitingForMedia && (
{t("video_tile.waiting_for_media")} - {showConnectioStats ? " " + rtcBackendIdentity : ""} + {showConnectionStats ? " " + rtcBackendIdentity : ""}
)} - {(audioStreamStats || videoStreamStats) && ( + {showConnectionStats && ( <> { }); test("SpotlightTile is accessible", async () => { - const vm1 = createRemoteMedia( + const vm1 = mockRemoteMedia( mockRtcMembership("@alice:example.org", "AAAA"), { rawDisplayName: "Alice", @@ -53,7 +53,7 @@ test("SpotlightTile is accessible", async () => { mockRemoteParticipant({}), ); - const vm2 = createLocalMedia( + const vm2 = mockLocalMedia( mockRtcMembership("@bob:example.org", "BBBB"), { rawDisplayName: "Bob", diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 2d1e3f54..ba99f826 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -33,20 +33,19 @@ import FullScreenMaximiseIcon from "../icons/FullScreenMaximise.svg?react"; import FullScreenMinimiseIcon from "../icons/FullScreenMinimise.svg?react"; import { MediaView } from "./MediaView"; import styles from "./SpotlightTile.module.css"; -import { - type EncryptionStatus, - LocalUserMediaViewModel, - type MediaViewModel, - ScreenShareViewModel, - type UserMediaViewModel, - type RemoteUserMediaViewModel, -} from "../state/MediaViewModel"; import { useInitial } from "../useInitial"; import { useMergedRefs } from "../useMergedRefs"; import { useReactiveState } from "../useReactiveState"; import { useLatest } from "../useLatest"; import { type SpotlightTileViewModel } from "../state/TileViewModel"; import { useBehavior } from "../useBehavior"; +import { type EncryptionStatus } from "../state/media/MemberMediaViewModel"; +import { type LocalUserMediaViewModel } from "../state/media/LocalUserMediaViewModel"; +import { type RemoteUserMediaViewModel } from "../state/media/RemoteUserMediaViewModel"; +import { type UserMediaViewModel } from "../state/media/UserMediaViewModel"; +import { type ScreenShareViewModel } from "../state/media/ScreenShareViewModel"; +import { type RemoteScreenShareViewModel } from "../state/media/RemoteScreenShareViewModel"; +import { type MediaViewModel } from "../state/media/MediaViewModel"; interface SpotlightItemBaseProps { ref?: Ref; @@ -55,7 +54,6 @@ interface SpotlightItemBaseProps { targetWidth: number; targetHeight: number; video: TrackReferenceOrPlaceholder | undefined; - videoEnabled: boolean; userId: string; unencryptedWarning: boolean; encryptionStatus: EncryptionStatus; @@ -68,6 +66,7 @@ interface SpotlightItemBaseProps { interface SpotlightUserMediaItemBaseProps extends SpotlightItemBaseProps { videoFit: "contain" | "cover"; + videoEnabled: boolean; } interface SpotlightLocalUserMediaItemProps extends SpotlightUserMediaItemBaseProps { @@ -107,14 +106,16 @@ const SpotlightUserMediaItem: FC = ({ ...props }) => { const videoFit = useBehavior(vm.videoFit$); + const videoEnabled = useBehavior(vm.videoEnabled$); const baseProps: SpotlightUserMediaItemBaseProps & RefAttributes = { videoFit, + videoEnabled, ...props, }; - return vm instanceof LocalUserMediaViewModel ? ( + return vm.local ? ( ) : ( @@ -123,6 +124,31 @@ const SpotlightUserMediaItem: FC = ({ SpotlightUserMediaItem.displayName = "SpotlightUserMediaItem"; +interface SpotlightScreenShareItemProps extends SpotlightItemBaseProps { + vm: ScreenShareViewModel; + videoEnabled: boolean; +} + +const SpotlightScreenShareItem: FC = ({ + vm, + ...props +}) => { + return ; +}; + +interface SpotlightRemoteScreenShareItemProps extends SpotlightItemBaseProps { + vm: RemoteScreenShareViewModel; +} + +const SpotlightRemoteScreenShareItem: FC< + SpotlightRemoteScreenShareItemProps +> = ({ vm, ...props }) => { + const videoEnabled = useBehavior(vm.videoEnabled$); + return ( + + ); +}; + interface SpotlightItemProps { ref?: Ref; vm: MediaViewModel; @@ -168,7 +194,6 @@ const SpotlightItem: FC = ({ const displayName = useBehavior(vm.displayName$); const mxcAvatarUrl = useBehavior(vm.mxcAvatarUrl$); const video = useBehavior(vm.video$); - const videoEnabled = useBehavior(vm.videoEnabled$); const unencryptedWarning = useBehavior(vm.unencryptedWarning$); const encryptionStatus = useBehavior(vm.encryptionStatus$); @@ -194,7 +219,6 @@ const SpotlightItem: FC = ({ targetWidth, targetHeight, video: video ?? undefined, - videoEnabled, userId: vm.userId, unencryptedWarning, focusUrl, @@ -205,10 +229,12 @@ const SpotlightItem: FC = ({ "aria-hidden": ariaHidden, }; - return vm instanceof ScreenShareViewModel ? ( - + if (vm.type === "user") + return ; + return vm.local ? ( + ) : ( - + ); }; diff --git a/src/utils/observable.ts b/src/utils/observable.ts index 2e19748b..353dc877 100644 --- a/src/utils/observable.ts +++ b/src/utils/observable.ts @@ -61,6 +61,20 @@ export function accumulate( events$.pipe(scan(update, initial), startWith(initial)); } +/** + * Given a source of toggle events, creates a Behavior whose value toggles + * between `true` and `false`. + */ +export function createToggle$( + scope: ObservableScope, + initialValue: boolean, + toggle$: Observable, +): Behavior { + return scope.behavior( + toggle$.pipe(accumulate(initialValue, (state) => !state)), + ); +} + const switchSymbol = Symbol("switch"); /** diff --git a/src/utils/test.ts b/src/utils/test.ts index d78bdf42..c1e67927 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -52,10 +52,6 @@ import { } from "matrix-js-sdk/lib/matrixrtc/IKeyTransport"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; -import { - LocalUserMediaViewModel, - RemoteUserMediaViewModel, -} from "../state/MediaViewModel"; import { E2eeType } from "../e2ee/e2eeType"; import { DEFAULT_CONFIG, @@ -66,6 +62,14 @@ import { type MediaDevices } from "../state/MediaDevices"; import { type Behavior, constant } from "../state/Behavior"; import { ObservableScope } from "../state/ObservableScope"; import { MuteStates } from "../state/MuteStates"; +import { + createLocalUserMedia, + type LocalUserMediaViewModel, +} from "../state/media/LocalUserMediaViewModel"; +import { + createRemoteUserMedia, + type RemoteUserMediaViewModel, +} from "../state/media/RemoteUserMediaViewModel"; export function withFakeTimers(continuation: () => void): void { vi.useFakeTimers(); @@ -323,30 +327,27 @@ export function mockLocalParticipant( } as Partial as LocalParticipant; } -export function createLocalMedia( +export function mockLocalMedia( rtcMember: CallMembership, roomMember: Partial, localParticipant: LocalParticipant, mediaDevices: MediaDevices, ): LocalUserMediaViewModel { const member = mockMatrixRoomMember(rtcMember, roomMember); - return new LocalUserMediaViewModel( - testScope(), - "local", - member.userId, - rtcMember.rtcBackendIdentity, - constant(localParticipant), - { - kind: E2eeType.PER_PARTICIPANT, - }, - constant(mockLivekitRoom({ localParticipant })), - constant("https://rtc-example.org"), + return createLocalUserMedia(testScope(), { + id: "local", + userId: member.userId, + rtcBackendIdentity: rtcMember.rtcBackendIdentity, + participant$: constant(localParticipant), + encryptionSystem: { kind: E2eeType.PER_PARTICIPANT }, + livekitRoom$: constant(mockLivekitRoom({ localParticipant })), + focusUrl$: constant("https://rtc-example.org"), mediaDevices, - constant(member.rawDisplayName ?? "nodisplayname"), - constant(member.getMxcAvatarUrl()), - constant(null), - constant(null), - ); + displayName$: constant(member.rawDisplayName ?? "nodisplayname"), + mxcAvatarUrl$: constant(member.getMxcAvatarUrl()), + handRaised$: constant(null), + reaction$: constant(null), + }); } export function mockRemoteParticipant( @@ -364,7 +365,7 @@ export function mockRemoteParticipant( } as RemoteParticipant; } -export function createRemoteMedia( +export function mockRemoteMedia( rtcMember: CallMembership, roomMember: Partial, participant: RemoteParticipant | null, @@ -376,23 +377,20 @@ export function createRemoteMedia( ), ): RemoteUserMediaViewModel { const member = mockMatrixRoomMember(rtcMember, roomMember); - return new RemoteUserMediaViewModel( - testScope(), - "remote", - member.userId, - rtcMember.rtcBackendIdentity, - constant(participant), - { - kind: E2eeType.PER_PARTICIPANT, - }, - constant(livekitRoom), - constant("https://rtc-example.org"), - constant(false), - constant(member.rawDisplayName ?? "nodisplayname"), - constant(member.getMxcAvatarUrl()), - constant(null), - constant(null), - ); + return createRemoteUserMedia(testScope(), { + id: "remote", + userId: member.userId, + rtcBackendIdentity: rtcMember.rtcBackendIdentity, + participant$: constant(participant), + encryptionSystem: { kind: E2eeType.PER_PARTICIPANT }, + livekitRoom$: constant(livekitRoom), + focusUrl$: constant("https://rtc-example.org"), + pretendToBeDisconnected$: constant(false), + displayName$: constant(member.rawDisplayName ?? "nodisplayname"), + mxcAvatarUrl$: constant(member.getMxcAvatarUrl()), + handRaised$: constant(null), + reaction$: constant(null), + }); } export function mockConfig( diff --git a/src/utils/videoFit.ts b/src/utils/videoFit.ts index fdd91be7..c7b18f03 100644 --- a/src/utils/videoFit.ts +++ b/src/utils/videoFit.ts @@ -17,7 +17,7 @@ import { type Behavior } from "../state/Behavior.ts"; import { observeInboundRtpStreamStats$, observeOutboundRtpStreamStats$, -} from "../state/MediaViewModel.ts"; +} from "../state/media/observeRtpStreamStats"; type Size = { width: number; diff --git a/yarn.lock b/yarn.lock index b1d27dec..4675d0e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -115,6 +115,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/code-frame@npm:7.29.0" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.28.5" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.1.1" + checksum: 10c0/d34cc504e7765dfb576a663d97067afb614525806b5cad1a5cc1a7183b916fec8ff57fa233585e3926fd5a9e6b31aae6df91aa81ae9775fb7a28f658d3346f0d + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.27.2": version: 7.27.3 resolution: "@babel/compat-data@npm:7.27.3" @@ -122,40 +133,33 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.27.7": - version: 7.28.0 - resolution: "@babel/compat-data@npm:7.28.0" - checksum: 10c0/c4e527302bcd61052423f757355a71c3bc62362bac13f7f130de16e439716f66091ff5bdecda418e8fa0271d4c725f860f0ee23ab7bf6e769f7a8bb16dfcb531 - languageName: node - linkType: hard - -"@babel/compat-data@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/compat-data@npm:7.28.5" - checksum: 10c0/702a25de73087b0eba325c1d10979eed7c9b6662677386ba7b5aa6eace0fc0676f78343bae080a0176ae26f58bd5535d73b9d0fbb547fef377692e8b249353a7 +"@babel/compat-data@npm:^7.28.6, @babel/compat-data@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/compat-data@npm:7.29.0" + checksum: 10c0/08f348554989d23aa801bf1405aa34b15e841c0d52d79da7e524285c77a5f9d298e70e11d91cc578d8e2c9542efc586d50c5f5cf8e1915b254a9dcf786913a94 languageName: node linkType: hard "@babel/core@npm:^7.16.5, @babel/core@npm:^7.18.5, @babel/core@npm:^7.21.3, @babel/core@npm:^7.28.0": - version: 7.28.5 - resolution: "@babel/core@npm:7.28.5" + version: 7.29.0 + resolution: "@babel/core@npm:7.29.0" dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.5" - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-module-transforms": "npm:^7.28.3" - "@babel/helpers": "npm:^7.28.4" - "@babel/parser": "npm:^7.28.5" - "@babel/template": "npm:^7.27.2" - "@babel/traverse": "npm:^7.28.5" - "@babel/types": "npm:^7.28.5" + "@babel/code-frame": "npm:^7.29.0" + "@babel/generator": "npm:^7.29.0" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helpers": "npm:^7.28.6" + "@babel/parser": "npm:^7.29.0" + "@babel/template": "npm:^7.28.6" + "@babel/traverse": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" "@jridgewell/remapping": "npm:^2.3.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10c0/535f82238027621da6bdffbdbe896ebad3558b311d6f8abc680637a9859b96edbf929ab010757055381570b29cf66c4a295b5618318d27a4273c0e2033925e72 + checksum: 10c0/5127d2e8e842ae409e11bcbb5c2dff9874abf5415e8026925af7308e903f4f43397341467a130490d1a39884f461bc2b67f3063bce0be44340db89687fd852aa languageName: node linkType: hard @@ -185,19 +189,6 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/generator@npm:7.28.0" - dependencies: - "@babel/parser": "npm:^7.28.0" - "@babel/types": "npm:^7.28.0" - "@jridgewell/gen-mapping": "npm:^0.3.12" - "@jridgewell/trace-mapping": "npm:^0.3.28" - jsesc: "npm:^3.0.2" - checksum: 10c0/1b3d122268ea3df50fde707ad864d9a55c72621357d5cebb972db3dd76859c45810c56e16ad23123f18f80cc2692f5a015d2858361300f0f224a05dc43d36a92 - languageName: node - linkType: hard - "@babel/generator@npm:^7.28.5": version: 7.28.5 resolution: "@babel/generator@npm:7.28.5" @@ -211,6 +202,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.29.0": + version: 7.29.1 + resolution: "@babel/generator@npm:7.29.1" + dependencies: + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10c0/349086e6876258ef3fb2823030fee0f6c0eb9c3ebe35fc572e16997f8c030d765f636ddc6299edae63e760ea6658f8ee9a2edfa6d6b24c9a80c917916b973551 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-annotate-as-pure@npm:7.25.9" @@ -229,7 +233,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.27.2": +"@babel/helper-compilation-targets@npm:^7.27.1": version: 7.27.2 resolution: "@babel/helper-compilation-targets@npm:7.27.2" dependencies: @@ -242,24 +246,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-create-class-features-plugin@npm:7.27.1" +"@babel/helper-compilation-targets@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-compilation-targets@npm:7.28.6" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.27.1" - "@babel/helper-member-expression-to-functions": "npm:^7.27.1" - "@babel/helper-optimise-call-expression": "npm:^7.27.1" - "@babel/helper-replace-supers": "npm:^7.27.1" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.1" + "@babel/compat-data": "npm:^7.28.6" + "@babel/helper-validator-option": "npm:^7.27.1" + browserslist: "npm:^4.24.0" + lru-cache: "npm:^5.1.1" semver: "npm:^6.3.1" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10c0/4ee199671d6b9bdd4988aa2eea4bdced9a73abfc831d81b00c7634f49a8fc271b3ceda01c067af58018eb720c6151322015d463abea7072a368ee13f35adbb4c + checksum: 10c0/3fcdf3b1b857a1578e99d20508859dbd3f22f3c87b8a0f3dc540627b4be539bae7f6e61e49d931542fe5b557545347272bbdacd7f58a5c77025a18b745593a50 languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.28.3, @babel/helper-create-class-features-plugin@npm:^7.28.5": +"@babel/helper-create-class-features-plugin@npm:^7.28.5": version: 7.28.5 resolution: "@babel/helper-create-class-features-plugin@npm:7.28.5" dependencies: @@ -276,6 +276,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-member-expression-to-functions": "npm:^7.28.5" + "@babel/helper-optimise-call-expression": "npm:^7.27.1" + "@babel/helper-replace-supers": "npm:^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.6" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/0b62b46717891f4366006b88c9b7f277980d4f578c4c3789b7a4f5a2e09e121de4cda9a414ab403986745cd3ad1af3fe2d948c9f78ab80d4dc085afc9602af50 + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6": version: 7.26.3 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.26.3" @@ -302,18 +319,31 @@ __metadata: languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.6.5": - version: 0.6.5 - resolution: "@babel/helper-define-polyfill-provider@npm:0.6.5" +"@babel/helper-create-regexp-features-plugin@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" dependencies: - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-plugin-utils": "npm:^7.27.1" - debug: "npm:^4.4.1" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + regexpu-core: "npm:^6.3.1" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/7af3d604cadecdb2b0d2cedd696507f02a53a58be0523281c2d6766211443b55161dde1e6c0d96ab16ddfd82a2607a2f792390caa24797e9733631f8aa86859f + languageName: node + linkType: hard + +"@babel/helper-define-polyfill-provider@npm:^0.6.6": + version: 0.6.6 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.6" + dependencies: + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + debug: "npm:^4.4.3" lodash.debounce: "npm:^4.0.8" - resolve: "npm:^1.22.10" + resolve: "npm:^1.22.11" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10c0/4886a068d9ca1e70af395340656a9dda33c50502c67eed39ff6451785f370bdfc6e57095b90cb92678adcd4a111ca60909af53d3a741120719c5604346ae409e + checksum: 10c0/1293d6f54d4ebb10c9e947e54de1aaa23b00233e19aca9790072f1893bf143af01442613f7b413300be7016d8e41b550af77acab28e7fa5fb796b2a175c528a1 languageName: node linkType: hard @@ -354,6 +384,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-module-imports@npm:7.28.6" + dependencies: + "@babel/traverse": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + checksum: 10c0/b49d8d8f204d9dbfd5ac70c54e533e5269afb3cea966a9d976722b13e9922cc773a653405f53c89acb247d5aebdae4681d631a3ae3df77ec046b58da76eda2ac + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.27.1": version: 7.27.3 resolution: "@babel/helper-module-transforms@npm:7.27.3" @@ -367,16 +407,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.28.3": - version: 7.28.3 - resolution: "@babel/helper-module-transforms@npm:7.28.3" +"@babel/helper-module-transforms@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-module-transforms@npm:7.28.6" dependencies: - "@babel/helper-module-imports": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.3" + "@babel/helper-module-imports": "npm:^7.28.6" + "@babel/helper-validator-identifier": "npm:^7.28.5" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/549be62515a6d50cd4cfefcab1b005c47f89bd9135a22d602ee6a5e3a01f27571868ada10b75b033569f24dc4a2bb8d04bfa05ee75c16da7ade2d0db1437fcdb + checksum: 10c0/6f03e14fc30b287ce0b839474b5f271e72837d0cafe6b172d759184d998fbee3903a035e81e07c2c596449e504f453463d58baa65b6f40a37ded5bec74620b2b languageName: node linkType: hard @@ -403,6 +443,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-plugin-utils@npm:7.28.6" + checksum: 10c0/3f5f8acc152fdbb69a84b8624145ff4f9b9f6e776cb989f9f968f8606eb7185c5c3cfcf3ba08534e37e1e0e1c118ac67080610333f56baa4f7376c99b5f1143d + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1" @@ -429,6 +476,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-replace-supers@npm:7.28.6" + dependencies: + "@babel/helper-member-expression-to-functions": "npm:^7.28.5" + "@babel/helper-optimise-call-expression": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.6" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/04663c6389551b99b8c3e7ba4e2638b8ca2a156418c26771516124c53083aa8e74b6a45abe5dd46360af79709a0e9c6b72c076d0eab9efecdd5aaf836e79d8d5 + languageName: node + linkType: hard + "@babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1" @@ -492,13 +552,13 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.28.4": - version: 7.28.4 - resolution: "@babel/helpers@npm:7.28.4" +"@babel/helpers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helpers@npm:7.28.6" dependencies: - "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.4" - checksum: 10c0/aaa5fb8098926dfed5f223adf2c5e4c7fbba4b911b73dfec2d7d3083f8ba694d201a206db673da2d9b3ae8c01793e795767654558c450c8c14b4c2175b4fcb44 + "@babel/template": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + checksum: 10c0/c4a779c66396bb0cf619402d92f1610601ff3832db2d3b86b9c9dd10983bf79502270e97ac6d5280cea1b1a37de2f06ecbac561bd2271545270407fbe64027cb languageName: node linkType: hard @@ -558,17 +618,6 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/parser@npm:7.28.0" - dependencies: - "@babel/types": "npm:^7.28.0" - bin: - parser: ./bin/babel-parser.js - checksum: 10c0/c2ef81d598990fa949d1d388429df327420357cb5200271d0d0a2784f1e6d54afc8301eb8bdf96d8f6c77781e402da93c7dc07980fcc136ac5b9d5f1fce701b5 - languageName: node - linkType: hard - "@babel/parser@npm:^7.28.5": version: 7.28.5 resolution: "@babel/parser@npm:7.28.5" @@ -580,6 +629,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/parser@npm:7.29.0" + dependencies: + "@babel/types": "npm:^7.29.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/333b2aa761264b91577a74bee86141ef733f9f9f6d4fc52548e4847dc35dfbf821f58c46832c637bfa761a6d9909d6a68f7d1ed59e17e4ffbb958dc510c17b62 + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" @@ -627,15 +687,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.28.3": - version: 7.28.3 - resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.28.3" +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.3" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/3cdc27c4e08a632a58e62c6017369401976edf1cd9ae73fd9f0d6770ddd9accf40b494db15b66bab8db2a8d5dc5bab5ca8c65b19b81fdca955cd8cbbe24daadb + checksum: 10c0/f1a9194e8d1742081def7af748e9249eb5082c25d0ced292720a1f054895f99041c764a05f45af669a2c8898aeb79266058aedb0d3e1038963ad49be8288918a languageName: node linkType: hard @@ -648,25 +708,25 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.27.1" +"@babel/plugin-syntax-import-assertions@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/06a954ee672f7a7c44d52b6e55598da43a7064e80df219765c51c37a0692641277e90411028f7cae4f4d1dedeed084f0c453576fa421c35a81f1603c5e3e0146 + checksum: 10c0/f3b8bdccb9b4d3e3b9226684ca518e055399d05579da97dfe0160a38d65198cfe7dce809e73179d6463a863a040f980de32425a876d88efe4eda933d0d95982c languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.27.1" +"@babel/plugin-syntax-import-attributes@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/e66f7a761b8360419bbb93ab67d87c8a97465ef4637a985ff682ce7ba6918b34b29d81190204cf908d0933058ee7b42737423cd8a999546c21b3aabad4affa9a + checksum: 10c0/1be160e2c426faa74e5be2e30e39e8d0d8c543063bd5d06cd804f8751b8fbcb82ce824ca7f9ce4b09c003693f6c06a11ce503b7e34d85e1a259631e4c3f72ad2 languageName: node linkType: hard @@ -715,29 +775,29 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.28.0" +"@babel/plugin-transform-async-generator-functions@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.29.0" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-remap-async-to-generator": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.0" + "@babel/traverse": "npm:^7.29.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/739d577e649d7d7b9845dc309e132964327ab3eaea43ad04d04a7dcb977c63f9aa9a423d1ca39baf10939128d02f52e6fda39c834fb9f1753785b1497e72c4dc + checksum: 10c0/4080fc5e7dad7761bfebbb4fbe06bdfeb3a8bf0c027bcb4373e59e6b3dc7c5002eca7cbb1afba801d6439df8f92f7bcb3fb862e8fbbe43a9e59bb5653dcc0568 languageName: node linkType: hard -"@babel/plugin-transform-async-to-generator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-async-to-generator@npm:7.27.1" +"@babel/plugin-transform-async-to-generator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.28.6" dependencies: - "@babel/helper-module-imports": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-module-imports": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-remap-async-to-generator": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/e76b1f6f9c3bbf72e17d7639406d47f09481806de4db99a8de375a0bb40957ea309b20aa705f0c25ab1d7c845e3f365af67eafa368034521151a0e352a03ef2f + checksum: 10c0/2eb0826248587df6e50038f36194a138771a7df22581020451c7779edeaf9ef39bf47c5b7a20ae2645af6416e8c896feeca273317329652e84abd79a4ab920ad languageName: node linkType: hard @@ -752,78 +812,66 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/plugin-transform-block-scoping@npm:7.28.5" +"@babel/plugin-transform-block-scoping@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-block-scoping@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/6b098887b375c23813ccee7a00179501fc5f709b4ee5a4b2a5c5c9ef3b44cee49e240214b1a9b4ad2bd1911fab3335eac2f0a3c5f014938a1b61bec84cec4845 + checksum: 10c0/2e3e09e1f9770b56cef4dcbffddf262508fd03416072f815ac66b2b224a3a12cd285cfec12fc067f1add414e7db5ce6dafb5164a6e0fb1a728e6a97d0c6f6e9d languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-class-properties@npm:7.27.1" +"@babel/plugin-transform-class-properties@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-class-properties@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/cc0662633c0fe6df95819fef223506ddf26c369c8d64ab21a728d9007ec866bf9436a253909819216c24a82186b6ccbc1ec94d7aaf3f82df227c7c02fa6a704b + checksum: 10c0/c4327fcd730c239d9f173f9b695b57b801729e273b4848aef1f75818069dfd31d985d75175db188d947b9b1bbe5353dae298849042026a5e4fcf07582ff3f9f1 languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.28.3": - version: 7.28.3 - resolution: "@babel/plugin-transform-class-static-block@npm:7.28.3" +"@babel/plugin-transform-class-static-block@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-class-static-block@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.28.3" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.12.0 - checksum: 10c0/8c922a64f6f5b359f7515c89ef0037bad583b4484dfebc1f6bc1cf13462547aaceb19788827c57ec9a2d62495f34c4b471ca636bf61af00fdaea5e9642c82b60 + checksum: 10c0/dbe9b1fd302ae41b73186e17ac8d8ecf625ebc2416a91f2dc8013977a1bdf21e6ea288a83f084752b412242f3866e789d4fddeb428af323fe35b60e0fae4f98c languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.28.4": - version: 7.28.4 - resolution: "@babel/plugin-transform-classes@npm:7.28.4" +"@babel/plugin-transform-classes@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-classes@npm:7.28.6" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.27.3" - "@babel/helper-compilation-targets": "npm:^7.27.2" + "@babel/helper-compilation-targets": "npm:^7.28.6" "@babel/helper-globals": "npm:^7.28.0" - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/helper-replace-supers": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.4" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-replace-supers": "npm:^7.28.6" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/76687ed37216ff012c599870dc00183fb716f22e1a02fe9481943664c0e4d0d88c3da347dc3fe290d4728f4d47cd594ffa621d23845e2bb8ab446e586308e066 + checksum: 10c0/dc22f1f6eadab17305128fbf9cc5f30e87a51a77dd0a6d5498097994e8a9b9a90ab298c11edf2342acbeaac9edc9c601cad72eedcf4b592cd465a787d7f41490 languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-computed-properties@npm:7.27.1" +"@babel/plugin-transform-computed-properties@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-computed-properties@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/template": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/template": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/e09a12f8c8ae0e6a6144c102956947b4ec05f6c844169121d0ec4529c2d30ad1dc59fee67736193b87a402f44552c888a519a680a31853bdb4d34788c28af3b0 - languageName: node - linkType: hard - -"@babel/plugin-transform-destructuring@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-destructuring@npm:7.28.0" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/cc7ccafa952b3ff7888544d5688cfafaba78c69ce1e2f04f3233f4f78c9de5e46e9695f5ea42c085b0c0cfa39b10f366d362a2be245b6d35b66d3eb1d427ccb2 + checksum: 10c0/1e9893503ae6d651125701cc29450e87c0b873c8febebff19da75da9c40cfb7968c52c28bf948244e461110aeb7b3591f2cc199b7406ff74a24c50c7a5729f39 languageName: node linkType: hard @@ -839,15 +887,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dotall-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-dotall-regex@npm:7.27.1" +"@babel/plugin-transform-dotall-regex@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/f9caddfad9a551b4dabe0dcb7c040f458fbaaa7bbb44200c20198b32c8259be8e050e58d2c853fdac901a4cfe490b86aa857036d8d461b192dd010d0e242dedb + checksum: 10c0/e2fb76b7ae99087cf4212013a3ca9dee07048f90f98fd6264855080fb6c3f169be11c9b8c9d8b26cf9a407e4d0a5fa6e103f7cef433a542b75cf7127c99d4f97 languageName: node linkType: hard @@ -862,15 +910,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.27.1" +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.29.0" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/121502a252b3206913e1e990a47fea34397b4cbf7804d4cd872d45961bc45b603423f60ca87f3a3023a62528f5feb475ac1c9ec76096899ec182fcb135eba375 + checksum: 10c0/6f03d9e5e31a05b28555541be6e283407e08447a36be6ddf8068b3efa970411d832e04b1282e2b894baf89a3864ff7e7f1e36346652a8d983170c6d548555167 languageName: node linkType: hard @@ -885,26 +933,26 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-explicit-resource-management@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-explicit-resource-management@npm:7.28.0" +"@babel/plugin-transform-explicit-resource-management@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-explicit-resource-management@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/plugin-transform-destructuring": "npm:^7.28.0" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/3baa706af3112adf2ae0c7ec0dc61b63dd02695eb5582f3c3a2b2d05399c6aa7756f55e7bbbd5412e613a6ba1dd6b6736904074b4d7ebd6b45a1e3f9145e4094 + checksum: 10c0/e6ea28c26e058fe61ada3e70b0def1992dd5a44f5fc14d8e2c6a3a512fb4d4c6dc96a3e1d0b466d83db32a9101e0b02df94051e48d3140da115b8ea9f8a31f37 languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.28.5" +"@babel/plugin-transform-exponentiation-operator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/006566e003c2a8175346cc4b3260fcd9f719b912ceae8a4e930ce02ee3cf0b2841d5c21795ba71790871783d3c0c1c3d22ce441b8819c37975844bfba027d3f7 + checksum: 10c0/4572d955a50dbc9a652a19431b4bb822cb479ee6045f4e6df72659c499c13036da0a2adf650b07ca995f2781e80aa868943bea1e7bff1de3169ec3f0a73a902e languageName: node linkType: hard @@ -944,14 +992,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-json-strings@npm:7.27.1" +"@babel/plugin-transform-json-strings@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-json-strings@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/2379714aca025516452a7c1afa1ca42a22b9b51a5050a653cc6198a51665ab82bdecf36106d32d731512706a1e373c5637f5ff635737319aa42f3827da2326d6 + checksum: 10c0/ab1091798c58e6c0bb8a864ee2b727c400924592c6ed69797a26b4c205f850a935de77ad516570be0419c279a3d9f7740c2aa448762eb8364ea77a6a357a9653 languageName: node linkType: hard @@ -966,14 +1014,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.5" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/fba4faa96d86fa745b0539bb631deee3f2296f0643c087a50ad0fac2e5f0a787fa885e9bdd90ae3e7832803f3c08e7cd3f1e830e7079dbdc023704923589bb23 + checksum: 10c0/4632a35453d2131f0be466681d0a33e3db44d868ff51ec46cd87e0ebd1e47c6a39b894f7d1c9b06f931addf6efa9d30e60c4cdedeb4f69d426f683e11f8490cf languageName: node linkType: hard @@ -1012,17 +1060,29 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.28.5" +"@babel/plugin-transform-modules-commonjs@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6" dependencies: - "@babel/helper-module-transforms": "npm:^7.28.3" - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.28.5" - "@babel/traverse": "npm:^7.28.5" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/7e8c0bcff79689702b974f6a0fedb5d0c6eeb5a5e3384deb7028e7cfe92a5242cc80e981e9c1817aad29f2ecc01841753365dd38d877aa0b91737ceec2acfd07 + checksum: 10c0/7c45992797c6150644c8552feff4a016ba7bd6d59ff2b039ed969a9c5b20a6804cd9d21db5045fc8cca8ca7f08262497e354e93f8f2be6a1cdf3fbfa8c31a9b6 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-systemjs@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.29.0" + dependencies: + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-validator-identifier": "npm:^7.28.5" + "@babel/traverse": "npm:^7.29.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/44ea502f2c990398b7d9adc5b44d9e1810a0a5e86eebc05c92d039458f0b3994fe243efa9353b90f8a648d8a91b79845fb353d8679d7324cc9de0162d732771d languageName: node linkType: hard @@ -1038,15 +1098,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.27.1" +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.29.0" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/8eaa8c9aee00a00f3bd8bd8b561d3f569644d98cb2cfe3026d7398aabf9b29afd62f24f142b4112fa1f572d9b0e1928291b099cde59f56d6b59f4d565e58abf2 + checksum: 10c0/1904db22da7f2bc3e380cd2c0786bda330ee1b1b3efa3f5203d980708c4bfeb5daa4dff48d01692193040bcc5f275dbdc0c2eadc8b1eb1b6dfe363564ad6e898 languageName: node linkType: hard @@ -1061,40 +1121,40 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1" +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/a435fc03aaa65c6ef8e99b2d61af0994eb5cdd4a28562d78c3b0b0228ca7e501aa255e1dff091a6996d7d3ea808eb5a65fd50ecd28dfb10687a8a1095dcadc7a + checksum: 10c0/6607f2201d66ccb688f0b1db09475ef995837df19f14705da41f693b669f834c206147a854864ab107913d7b4f4748878b0cd9fe9ca8bfd1bee0c206fc027b49 languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.27.1" +"@babel/plugin-transform-numeric-separator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/b72cbebbfe46fcf319504edc1cf59f3f41c992dd6840db766367f6a1d232cd2c52143c5eaf57e0316710bee251cae94be97c6d646b5022fcd9274ccb131b470c + checksum: 10c0/191097d8d2753cdd16d1acca65a945d1645ab20b65655c2f5b030a9e38967a52e093dcb21ebf391e342222705c6ffe5dea15dafd6257f7b51b77fb64a830b637 languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.28.4": - version: 7.28.4 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.4" +"@babel/plugin-transform-object-rest-spread@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.6" dependencies: - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-plugin-utils": "npm:^7.27.1" - "@babel/plugin-transform-destructuring": "npm:^7.28.0" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" "@babel/plugin-transform-parameters": "npm:^7.27.7" - "@babel/traverse": "npm:^7.28.4" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/81725c8d6349957899975f3f789b1d4fb050ee8b04468ebfaccd5b59e0bda15cbfdef09aee8b4359f322b6715149d680361f11c1a420c4bdbac095537ecf7a90 + checksum: 10c0/f55334352d4fcde385f2e8a58836687e71ff668c9b6e4c34d52575bf2789cdde92d9d3116edba13647ac0bc3e51fb2a6d1e8fb822dce7e8123334b82600bc4c3 languageName: node linkType: hard @@ -1110,14 +1170,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.27.1" +"@babel/plugin-transform-optional-catch-binding@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/807a4330f1fac08e2682d57bc82e714868fc651c8876f9a8b3a3fd8f53c129e87371f8243e712ac7dae11e090b737a2219a02fe1b6459a29e664fa073c3277bb + checksum: 10c0/36e8face000ee65e478a55febf687ce9be7513ad498c60dfe585851555565e0c28e7cb891b3c59709318539ce46f7697d5f42130eb18f385cd47e47cfa297446 languageName: node linkType: hard @@ -1133,15 +1193,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.5" +"@babel/plugin-transform-optional-chaining@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/adf5f70b1f9eb0dd6ff3d159a714683af3c910775653e667bd9f864c3dc2dc9872aba95f6c1e5f2a9675067241942f4fd0d641147ef4bf2bd8bc15f1fa0f2ed5 + checksum: 10c0/c159cc74115c2266be21791f192dd079e2aeb65c8731157e53b80fcefa41e8e28ad370021d4dfbdb31f25e5afa0322669a8eb2d032cd96e65ac37e020324c763 languageName: node linkType: hard @@ -1156,28 +1216,28 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-methods@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-private-methods@npm:7.27.1" +"@babel/plugin-transform-private-methods@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-private-methods@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/232bedfe9d28df215fb03cc7623bdde468b1246bdd6dc24465ff4bf9cc5f5a256ae33daea1fafa6cc59705e4d29da9024bb79baccaa5cd92811ac5db9b9244f2 + checksum: 10c0/fb504e2bfdcf3f734d2a90ab20d61427c58385f57f950d3de6ff4e6d12dd4aa7d552147312d218367e129b7920dccfc3230ba554de861986cda38921bad84067 languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.27.1" +"@babel/plugin-transform-private-property-in-object@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.28.6" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.27.1" - "@babel/helper-create-class-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/a8c4536273ca716dcc98e74ea25ca76431528554922f184392be3ddaf1761d4aa0e06f1311577755bd1613f7054fb51d29de2ada1130f743d329170a1aa1fe56 + checksum: 10c0/0f6bbc6ec3f93b556d3de7d56bf49335255fc4c43488e51a5025d6ee0286183fd3cf950ffcac1bbeed8a45777f860a49996455c8d3b4a04c3b1a5f28e697fe31 languageName: node linkType: hard @@ -1263,26 +1323,26 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.28.4": - version: 7.28.4 - resolution: "@babel/plugin-transform-regenerator@npm:7.28.4" +"@babel/plugin-transform-regenerator@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-regenerator@npm:7.29.0" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/5ad14647ffaac63c920e28df1b580ee2e932586bbdc71f61ec264398f68a5406c71a7f921de397a41b954a69316c5ab90e5d789ffa2bb34c5e6feb3727cfefb8 + checksum: 10c0/86c7db9b97f85ee47c0fae0528802cbc06e5775e61580ee905335c16bb971270086764a3859873d9adcd7d0f913a5b93eb0dc271aec8fb9e93e090e4ac95e29e languageName: node linkType: hard -"@babel/plugin-transform-regexp-modifiers@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.27.1" +"@babel/plugin-transform-regexp-modifiers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/31ae596ab56751cf43468a6c0a9d6bc3521d306d2bee9c6957cdb64bea53812ce24bd13a32f766150d62b737bca5b0650b2c62db379382fff0dccbf076055c33 + checksum: 10c0/97e36b086800f71694fa406abc00192e3833662f2bdd5f51c018bd0c95eef247c4ae187417c207d03a9c5374342eac0bb65a39112c431a9b23b09b1eda1562e5 languageName: node linkType: hard @@ -1308,15 +1368,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-spread@npm:7.27.1" +"@babel/plugin-transform-spread@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-spread@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/b34fc58b33bd35b47d67416655c2cbc8578fbb3948b4592bc15eb6d8b4046986e25c06e3b9929460fa4ab08e9653582415e7ef8b87d265e1239251bdf5a4c162 + checksum: 10c0/bcac50e558d6f0c501cbce19ec197af558cef51fe3b3a6eba27276e323e57a5be28109b4264a5425ac12a67bf95d6af9c2a42b05e79c522ce913fb9529259d76 languageName: node linkType: hard @@ -1379,15 +1439,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-property-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.27.1" +"@babel/plugin-transform-unicode-property-regex@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/a332bc3cb3eeea67c47502bc52d13a0f8abae5a7bfcb08b93a8300ddaff8d9e1238f912969494c1b494c1898c6f19687054440706700b6d12cb0b90d88beb4d0 + checksum: 10c0/b25f8cde643f4f47e0fa4f7b5c552e2dfbb6ad0ce07cf40f7e8ae40daa9855ad855d76d4d6d010153b74e48c8794685955c92ca637c0da152ce5f0fa9e7c90fa languageName: node linkType: hard @@ -1403,95 +1463,95 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-sets-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.27.1" +"@babel/plugin-transform-unicode-sets-regex@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/236645f4d0a1fba7c18dc8ffe3975933af93e478f2665650c2d91cf528cfa1587cde5cfe277e0e501fc03b5bf57638369575d6539cef478632fb93bd7d7d7178 + checksum: 10c0/c03c8818736b138db73d1f7a96fbfa22d1994639164d743f0f00e6383d3b7b3144d333de960ff4afad0bddd0baaac257295e3316969eba995b1b6a1b4dec933e languageName: node linkType: hard "@babel/preset-env@npm:^7.22.20": - version: 7.28.5 - resolution: "@babel/preset-env@npm:7.28.5" + version: 7.29.0 + resolution: "@babel/preset-env@npm:7.29.0" dependencies: - "@babel/compat-data": "npm:^7.28.5" - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/compat-data": "npm:^7.29.0" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-validator-option": "npm:^7.27.1" "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.28.5" "@babel/plugin-bugfix-safari-class-field-initializer-scope": "npm:^7.27.1" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.27.1" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.27.1" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.28.3" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.28.6" "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions": "npm:^7.27.1" - "@babel/plugin-syntax-import-attributes": "npm:^7.27.1" + "@babel/plugin-syntax-import-assertions": "npm:^7.28.6" + "@babel/plugin-syntax-import-attributes": "npm:^7.28.6" "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" "@babel/plugin-transform-arrow-functions": "npm:^7.27.1" - "@babel/plugin-transform-async-generator-functions": "npm:^7.28.0" - "@babel/plugin-transform-async-to-generator": "npm:^7.27.1" + "@babel/plugin-transform-async-generator-functions": "npm:^7.29.0" + "@babel/plugin-transform-async-to-generator": "npm:^7.28.6" "@babel/plugin-transform-block-scoped-functions": "npm:^7.27.1" - "@babel/plugin-transform-block-scoping": "npm:^7.28.5" - "@babel/plugin-transform-class-properties": "npm:^7.27.1" - "@babel/plugin-transform-class-static-block": "npm:^7.28.3" - "@babel/plugin-transform-classes": "npm:^7.28.4" - "@babel/plugin-transform-computed-properties": "npm:^7.27.1" + "@babel/plugin-transform-block-scoping": "npm:^7.28.6" + "@babel/plugin-transform-class-properties": "npm:^7.28.6" + "@babel/plugin-transform-class-static-block": "npm:^7.28.6" + "@babel/plugin-transform-classes": "npm:^7.28.6" + "@babel/plugin-transform-computed-properties": "npm:^7.28.6" "@babel/plugin-transform-destructuring": "npm:^7.28.5" - "@babel/plugin-transform-dotall-regex": "npm:^7.27.1" + "@babel/plugin-transform-dotall-regex": "npm:^7.28.6" "@babel/plugin-transform-duplicate-keys": "npm:^7.27.1" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.29.0" "@babel/plugin-transform-dynamic-import": "npm:^7.27.1" - "@babel/plugin-transform-explicit-resource-management": "npm:^7.28.0" - "@babel/plugin-transform-exponentiation-operator": "npm:^7.28.5" + "@babel/plugin-transform-explicit-resource-management": "npm:^7.28.6" + "@babel/plugin-transform-exponentiation-operator": "npm:^7.28.6" "@babel/plugin-transform-export-namespace-from": "npm:^7.27.1" "@babel/plugin-transform-for-of": "npm:^7.27.1" "@babel/plugin-transform-function-name": "npm:^7.27.1" - "@babel/plugin-transform-json-strings": "npm:^7.27.1" + "@babel/plugin-transform-json-strings": "npm:^7.28.6" "@babel/plugin-transform-literals": "npm:^7.27.1" - "@babel/plugin-transform-logical-assignment-operators": "npm:^7.28.5" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.28.6" "@babel/plugin-transform-member-expression-literals": "npm:^7.27.1" "@babel/plugin-transform-modules-amd": "npm:^7.27.1" - "@babel/plugin-transform-modules-commonjs": "npm:^7.27.1" - "@babel/plugin-transform-modules-systemjs": "npm:^7.28.5" + "@babel/plugin-transform-modules-commonjs": "npm:^7.28.6" + "@babel/plugin-transform-modules-systemjs": "npm:^7.29.0" "@babel/plugin-transform-modules-umd": "npm:^7.27.1" - "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.29.0" "@babel/plugin-transform-new-target": "npm:^7.27.1" - "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.27.1" - "@babel/plugin-transform-numeric-separator": "npm:^7.27.1" - "@babel/plugin-transform-object-rest-spread": "npm:^7.28.4" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.28.6" + "@babel/plugin-transform-numeric-separator": "npm:^7.28.6" + "@babel/plugin-transform-object-rest-spread": "npm:^7.28.6" "@babel/plugin-transform-object-super": "npm:^7.27.1" - "@babel/plugin-transform-optional-catch-binding": "npm:^7.27.1" - "@babel/plugin-transform-optional-chaining": "npm:^7.28.5" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.28.6" + "@babel/plugin-transform-optional-chaining": "npm:^7.28.6" "@babel/plugin-transform-parameters": "npm:^7.27.7" - "@babel/plugin-transform-private-methods": "npm:^7.27.1" - "@babel/plugin-transform-private-property-in-object": "npm:^7.27.1" + "@babel/plugin-transform-private-methods": "npm:^7.28.6" + "@babel/plugin-transform-private-property-in-object": "npm:^7.28.6" "@babel/plugin-transform-property-literals": "npm:^7.27.1" - "@babel/plugin-transform-regenerator": "npm:^7.28.4" - "@babel/plugin-transform-regexp-modifiers": "npm:^7.27.1" + "@babel/plugin-transform-regenerator": "npm:^7.29.0" + "@babel/plugin-transform-regexp-modifiers": "npm:^7.28.6" "@babel/plugin-transform-reserved-words": "npm:^7.27.1" "@babel/plugin-transform-shorthand-properties": "npm:^7.27.1" - "@babel/plugin-transform-spread": "npm:^7.27.1" + "@babel/plugin-transform-spread": "npm:^7.28.6" "@babel/plugin-transform-sticky-regex": "npm:^7.27.1" "@babel/plugin-transform-template-literals": "npm:^7.27.1" "@babel/plugin-transform-typeof-symbol": "npm:^7.27.1" "@babel/plugin-transform-unicode-escapes": "npm:^7.27.1" - "@babel/plugin-transform-unicode-property-regex": "npm:^7.27.1" + "@babel/plugin-transform-unicode-property-regex": "npm:^7.28.6" "@babel/plugin-transform-unicode-regex": "npm:^7.27.1" - "@babel/plugin-transform-unicode-sets-regex": "npm:^7.27.1" + "@babel/plugin-transform-unicode-sets-regex": "npm:^7.28.6" "@babel/preset-modules": "npm:0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2: "npm:^0.4.14" - babel-plugin-polyfill-corejs3: "npm:^0.13.0" - babel-plugin-polyfill-regenerator: "npm:^0.6.5" - core-js-compat: "npm:^3.43.0" + babel-plugin-polyfill-corejs2: "npm:^0.4.15" + babel-plugin-polyfill-corejs3: "npm:^0.14.0" + babel-plugin-polyfill-regenerator: "npm:^0.6.6" + core-js-compat: "npm:^3.48.0" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/d1b730158de290f1c54ed7db0f4fed3f82db5f868ab0a4cb3fc2ea76ed683b986ae136f6e7eb0b44b91bc9a99039a2559851656b4fd50193af1a815a3e32e524 + checksum: 10c0/08737e333a538703ba20e9e93b5bfbc01abbb9d3b2519b5b62ad05d3b6b92d79445b1dac91229b8cfcfb0b681b22b7c6fa88d7c1cc15df1690a23b21287f55b6 languageName: node linkType: hard @@ -1573,7 +1633,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4": +"@babel/runtime@npm:^7.28.4": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7 @@ -1602,6 +1662,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/template@npm:7.28.6" + dependencies: + "@babel/code-frame": "npm:^7.28.6" + "@babel/parser": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + checksum: 10c0/66d87225ed0bc77f888181ae2d97845021838c619944877f7c4398c6748bcf611f216dfd6be74d39016af502bca876e6ce6873db3c49e4ac354c56d34d57e9f5 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.10.3": version: 7.25.9 resolution: "@babel/traverse@npm:7.25.9" @@ -1632,22 +1703,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/traverse@npm:7.28.0" - dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.0" - "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.28.0" - "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.0" - debug: "npm:^4.3.1" - checksum: 10c0/32794402457827ac558173bcebdcc0e3a18fa339b7c41ca35621f9f645f044534d91bb923ff385f5f960f2e495f56ce18d6c7b0d064d2f0ccb55b285fa6bc7b9 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5": +"@babel/traverse@npm:^7.28.5": version: 7.28.5 resolution: "@babel/traverse@npm:7.28.5" dependencies: @@ -1662,6 +1718,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/traverse@npm:7.29.0" + dependencies: + "@babel/code-frame": "npm:^7.29.0" + "@babel/generator": "npm:^7.29.0" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.29.0" + "@babel/template": "npm:^7.28.6" + "@babel/types": "npm:^7.29.0" + debug: "npm:^4.3.1" + checksum: 10c0/f63ef6e58d02a9fbf3c0e2e5f1c877da3e0bc57f91a19d2223d53e356a76859cbaf51171c9211c71816d94a0e69efa2732fd27ffc0e1bbc84b636e60932333eb + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.10.3, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3": version: 7.26.0 resolution: "@babel/types@npm:7.26.0" @@ -1702,17 +1773,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.28.0": - version: 7.28.2 - resolution: "@babel/types@npm:7.28.2" - dependencies: - "@babel/helper-string-parser": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10c0/24b11c9368e7e2c291fe3c1bcd1ed66f6593a3975f479cbb9dd7b8c8d8eab8a962b0d2fca616c043396ce82500ac7d23d594fbbbd013828182c01596370a0b10 - languageName: node - linkType: hard - -"@babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5": +"@babel/types@npm:^7.28.5": version: 7.28.5 resolution: "@babel/types@npm:7.28.5" dependencies: @@ -1722,6 +1783,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/types@npm:7.29.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.28.5" + checksum: 10c0/23cc3466e83bcbfab8b9bd0edaafdb5d4efdb88b82b3be6728bbade5ba2f0996f84f63b1c5f7a8c0d67efded28300898a5f930b171bb40b311bca2029c4e9b4f + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^1.0.2": version: 1.0.2 resolution: "@bcoe/v8-coverage@npm:1.0.2" @@ -2195,14 +2266,14 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-normalize-display-values@npm:^4.0.0": - version: 4.0.0 - resolution: "@csstools/postcss-normalize-display-values@npm:4.0.0" +"@csstools/postcss-normalize-display-values@npm:^4.0.1": + version: 4.0.1 + resolution: "@csstools/postcss-normalize-display-values@npm:4.0.1" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/d3a3a362b532163bd791f97348ef28b7a43baf01987c7702b06285e751cdc5ea3e3a2553f088260515b4d28263d5c475923d4d4780ecb4078ec66dff50c9e638 + checksum: 10c0/5d19364bad8554b047cebd94ad7e203723ed76abaf690e4b92c74e6fc7c3642cb8858ade3263da61aff26d97bb258af567b1036e97865b7aa3b17522241fd1e1 languageName: node linkType: hard @@ -2430,16 +2501,16 @@ __metadata: languageName: node linkType: hard -"@es-joy/jsdoccomment@npm:~0.76.0": - version: 0.76.0 - resolution: "@es-joy/jsdoccomment@npm:0.76.0" +"@es-joy/jsdoccomment@npm:~0.78.0": + version: 0.78.0 + resolution: "@es-joy/jsdoccomment@npm:0.78.0" dependencies: "@types/estree": "npm:^1.0.8" - "@typescript-eslint/types": "npm:^8.46.0" + "@typescript-eslint/types": "npm:^8.46.4" comment-parser: "npm:1.4.1" esquery: "npm:^1.6.0" - jsdoc-type-pratt-parser: "npm:~6.10.0" - checksum: 10c0/8fe4edec7d60562787ea8c77193ebe8737a9e28ec3143d383506b63890d0ffd45a2813e913ad1f00f227cb10e3a1fb913e5a696b33d499dc564272ff1a6f3fdb + jsdoc-type-pratt-parser: "npm:~7.0.0" + checksum: 10c0/be18b8149303e8e7c9414b0b0453a0fa959c1c8db6f721b75178336e01b65a9f251db98ecfedfb1b3cfa5e717f3e2abdb06a0f8dbe45d3330a62262c5331c327 languageName: node linkType: hard @@ -2829,21 +2900,21 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.7.0": - version: 4.7.0 - resolution: "@eslint-community/eslint-utils@npm:4.7.0" +"@eslint-community/eslint-utils@npm:^4.9.1": + version: 4.9.1 + resolution: "@eslint-community/eslint-utils@npm:4.9.1" dependencies: eslint-visitor-keys: "npm:^3.4.3" peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10c0/c0f4f2bd73b7b7a9de74b716a664873d08ab71ab439e51befe77d61915af41a81ecec93b408778b3a7856185244c34c2c8ee28912072ec14def84ba2dec70adf + checksum: 10c0/dc4ab5e3e364ef27e33666b11f4b86e1a6c1d7cbf16f0c6ff87b1619b3562335e9201a3d6ce806221887ff780ec9d828962a290bb910759fd40a674686503f02 languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0": - version: 4.12.1 - resolution: "@eslint-community/regexpp@npm:4.12.1" - checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6 +"@eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d languageName: node linkType: hard @@ -2989,15 +3060,15 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:3.0.7": - version: 3.0.7 - resolution: "@formatjs/ecma402-abstract@npm:3.0.7" +"@formatjs/ecma402-abstract@npm:3.1.1": + version: 3.1.1 + resolution: "@formatjs/ecma402-abstract@npm:3.1.1" dependencies: - "@formatjs/fast-memoize": "npm:3.0.2" - "@formatjs/intl-localematcher": "npm:0.7.4" - decimal.js: "npm:^10.4.3" - tslib: "npm:^2.8.0" - checksum: 10c0/0fdc25ef72dcd5bbe1deeb190be2f0a2e2770a2135904d16ddfb424305a1efed14b026fba6c48121bc32f693abf1fe08c0ee12cb7d888cb2ba92963236d82c77 + "@formatjs/fast-memoize": "npm:3.1.0" + "@formatjs/intl-localematcher": "npm:0.8.1" + decimal.js: "npm:^10.6.0" + tslib: "npm:^2.8.1" + checksum: 10c0/0b4aad9d3917e385d5b090dd1bf6c0a4600851d87149b6a2b552b4f7d31cdf348fcd19ec534cc79efb375997747ae17f9d09633121f4282fac3c5b1cce90ae98 languageName: node linkType: hard @@ -3010,23 +3081,23 @@ __metadata: languageName: node linkType: hard -"@formatjs/fast-memoize@npm:3.0.2": - version: 3.0.2 - resolution: "@formatjs/fast-memoize@npm:3.0.2" +"@formatjs/fast-memoize@npm:3.1.0": + version: 3.1.0 + resolution: "@formatjs/fast-memoize@npm:3.1.0" dependencies: - tslib: "npm:^2.8.0" - checksum: 10c0/f7d1074090df309d37322979fe5fc96451531317b42bd927102a3a86dee537b1cb0e378158c74e00efd9714a0aa0f1e5a673c749535df200e13167112676ce88 + tslib: "npm:^2.8.1" + checksum: 10c0/367cf8b2816117a3870224a56a3127f2fa5fb854f696102e1cb6229c2f6dec35ccb433fa5343cda76ee5a0a21bff977fad1e4a15f9fba06bcb11f5d4e76d8919 languageName: node linkType: hard -"@formatjs/intl-durationformat@npm:^0.9.0": - version: 0.9.1 - resolution: "@formatjs/intl-durationformat@npm:0.9.1" +"@formatjs/intl-durationformat@npm:^0.10.0": + version: 0.10.1 + resolution: "@formatjs/intl-durationformat@npm:0.10.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.7" - "@formatjs/intl-localematcher": "npm:0.7.4" - tslib: "npm:^2.8.0" - checksum: 10c0/6f7b01027c07162b26be3014bba17a7633d1f9cfe6c26c5f403e72b92ac26c67cda2d88aeedab891b080664cfb4aace0eacec70b6f006616fe0d322dc2e8145d + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/intl-localematcher": "npm:0.8.1" + tslib: "npm:^2.8.1" + checksum: 10c0/9b0863ba7dd1abc64bbbd15afd24b3bc5ccabe39ab13db97bdfe4d78de7861bd9b0b4a060d6087f1817184328f97b64bfb69ca119071d20dab8f3b5ad8cf9231 languageName: node linkType: hard @@ -3039,13 +3110,13 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.7.4": - version: 0.7.4 - resolution: "@formatjs/intl-localematcher@npm:0.7.4" +"@formatjs/intl-localematcher@npm:0.8.1": + version: 0.8.1 + resolution: "@formatjs/intl-localematcher@npm:0.8.1" dependencies: - "@formatjs/fast-memoize": "npm:3.0.2" - tslib: "npm:^2.8.0" - checksum: 10c0/7fc31e13397317faadee033dcf668cda49f031b28542c634c920339f374f483235543e08be2077152cfe5dd41e651d8d2d37b6ece8aa044c0998c48f5472fb1a + "@formatjs/fast-memoize": "npm:3.1.0" + tslib: "npm:^2.8.1" + checksum: 10c0/c1ecd407891dec31bc5e9cab7ac4294bfb8c9eb11a5e624d9ae81627fb4dbb27ce38b0efafcfd8b26981b3ea43d765de34238a50474d07fd9556d1e79cfbcc6b languageName: node linkType: hard @@ -3266,11 +3337,11 @@ __metadata: linkType: hard "@livekit/protocol@npm:^1.42.2": - version: 1.43.4 - resolution: "@livekit/protocol@npm:1.43.4" + version: 1.44.0 + resolution: "@livekit/protocol@npm:1.44.0" dependencies: "@bufbuild/protobuf": "npm:^1.10.0" - checksum: 10c0/38077ceec44151b7481a95ce25869570b1466359de4992d9367002fc5b0925fc8ca120ed448099ae552064f23664ebe0920669f4fba97164eacbf181664683f2 + checksum: 10c0/f547a5ee586cae002ed2834f0a823573e38887562dbc793e261791b0572472c6732262a5466c96082464575a3248a4c6cb0428420418e834cdbef1b202cddedf languageName: node linkType: hard @@ -3294,9 +3365,9 @@ __metadata: linkType: hard "@mediapipe/tasks-vision@npm:^0.10.18": - version: 0.10.21 - resolution: "@mediapipe/tasks-vision@npm:0.10.21" - checksum: 10c0/11b2bdf98b8cb6e044f2a954e7c8393169e62c86ff49b3d0b61c3b327d18e1ccd47a187999b023bad48380c9da41bfa66eb165301c80da07746390482cb18a19 + version: 0.10.32 + resolution: "@mediapipe/tasks-vision@npm:0.10.32" + checksum: 10c0/734d472ece8f10e8ba6bdcda7adfc46ddd4da737797e3699aabdb857a4ec5ae87de064b0d7ed41bd96fe49bb8a9420afd1fcc337eea38cd536e7b41bed9f88b7 languageName: node linkType: hard @@ -3774,13 +3845,13 @@ __metadata: linkType: hard "@playwright/test@npm:^1.57.0": - version: 1.57.0 - resolution: "@playwright/test@npm:1.57.0" + version: 1.58.2 + resolution: "@playwright/test@npm:1.58.2" dependencies: - playwright: "npm:1.57.0" + playwright: "npm:1.58.2" bin: playwright: cli.js - checksum: 10c0/35ba4b28be72bf0a53e33dbb11c6cff848fb9a37f49e893ce63a90675b5291ec29a1ba82c8a3b043abaead129400f0589623e9ace2e6a1c8eaa409721ecc3774 + checksum: 10c0/2164c03ad97c3653ff02e8818a71f3b2bbc344ac07924c9d8e31cd57505d6d37596015a41f51396b3ed8de6840f59143eaa9c21bf65515963da20740119811da languageName: node linkType: hard @@ -5548,8 +5619,8 @@ __metadata: linkType: hard "@testing-library/react@npm:^16.0.0": - version: 16.3.1 - resolution: "@testing-library/react@npm:16.3.1" + version: 16.3.2 + resolution: "@testing-library/react@npm:16.3.2" dependencies: "@babel/runtime": "npm:^7.12.5" peerDependencies: @@ -5563,7 +5634,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/5a26ceaa4ab1d065be722d93e3b019883864ae038f9fd1c974f5b8a173f5f35a25768ecb2baa02a783299f009cbcd09fa7ee0b8b3d360d1c0f81535436358b28 + checksum: 10c0/f9c7f0915e1b5f7b750e6c7d8b51f091b8ae7ea99bacb761d7b8505ba25de9cfcb749a0f779f1650fb268b499dd79165dc7e1ee0b8b4cb63430d3ddc81ffe044 languageName: node linkType: hard @@ -5740,20 +5811,20 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 25.0.3 - resolution: "@types/node@npm:25.0.3" + version: 25.3.0 + resolution: "@types/node@npm:25.3.0" dependencies: - undici-types: "npm:~7.16.0" - checksum: 10c0/b7568f0d765d9469621615e2bb257c7fd1953d95e9acbdb58dffb6627a2c4150d405a4600aa1ad8a40182a94fe5f903cafd3c0a2f5132814debd0e3bfd61f835 + undici-types: "npm:~7.18.0" + checksum: 10c0/7b2b18c9d68047157367fc2f786d4f166d22dc0ad9f82331ca02fb16f2f391854123dbe604dcb938cda119c87051e4bb71dcb9ece44a579f483a6f96d4bd41de languageName: node linkType: hard "@types/node@npm:^24.0.0": - version: 24.10.4 - resolution: "@types/node@npm:24.10.4" + version: 24.10.13 + resolution: "@types/node@npm:24.10.13" dependencies: undici-types: "npm:~7.16.0" - checksum: 10c0/069639cb7233ee747df1897b5e784f6b6c5da765c96c94773c580aac888fa1a585048d2a6e95eb8302d89c7a9df75801c8b5a0b7d0221d4249059cf09a5f4228 + checksum: 10c0/4ff0b9b060b5477c0fec5b11a176f294be588104ab546295db65b17a92ba0a6077b52ad92dd3c0d2154198c7f9d0021e6c1d42b00c9ac7ebfd85632afbcc48a4 languageName: node linkType: hard @@ -5790,11 +5861,11 @@ __metadata: linkType: hard "@types/react@npm:^19.0.0": - version: 19.2.7 - resolution: "@types/react@npm:19.2.7" + version: 19.2.14 + resolution: "@types/react@npm:19.2.14" dependencies: csstype: "npm:^3.2.2" - checksum: 10c0/a7b75f1f9fcb34badd6f84098be5e35a0aeca614bc91f93d2698664c0b2ba5ad128422bd470ada598238cebe4f9e604a752aead7dc6f5a92261d0c7f9b27cfd1 + checksum: 10c0/7d25bf41b57719452d86d2ac0570b659210402707313a36ee612666bf11275a1c69824f8c3ee1fdca077ccfe15452f6da8f1224529b917050eb2d861e52b59b7 languageName: node linkType: hard @@ -5850,22 +5921,22 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^8.31.0": - version: 8.51.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.51.0" + version: 8.56.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.56.1" dependencies: - "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.51.0" - "@typescript-eslint/type-utils": "npm:8.51.0" - "@typescript-eslint/utils": "npm:8.51.0" - "@typescript-eslint/visitor-keys": "npm:8.51.0" - ignore: "npm:^7.0.0" + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.56.1" + "@typescript-eslint/type-utils": "npm:8.56.1" + "@typescript-eslint/utils": "npm:8.56.1" + "@typescript-eslint/visitor-keys": "npm:8.56.1" + ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^2.2.0" + ts-api-utils: "npm:^2.4.0" peerDependencies: - "@typescript-eslint/parser": ^8.51.0 - eslint: ^8.57.0 || ^9.0.0 + "@typescript-eslint/parser": ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/3140e66a0f722338d56bf3de2b7cbb9a74a812d8da90fc61975ea029f6a401252c0824063d4c4baab9827de6f0209b34f4bbdc46e3f5fefd8fa2ff4a3980406f + checksum: 10c0/8a97e777792ee3e25078884ba0a04f6732367779c9487abcdc5a2d65b224515fa6a0cf1fac1aafc52fb30f3af97f2e1c9949aadbd6ca74a0165691f95494a721 languageName: node linkType: hard @@ -5881,31 +5952,31 @@ __metadata: linkType: hard "@typescript-eslint/parser@npm:^8.31.0": - version: 8.51.0 - resolution: "@typescript-eslint/parser@npm:8.51.0" + version: 8.56.1 + resolution: "@typescript-eslint/parser@npm:8.56.1" dependencies: - "@typescript-eslint/scope-manager": "npm:8.51.0" - "@typescript-eslint/types": "npm:8.51.0" - "@typescript-eslint/typescript-estree": "npm:8.51.0" - "@typescript-eslint/visitor-keys": "npm:8.51.0" - debug: "npm:^4.3.4" + "@typescript-eslint/scope-manager": "npm:8.56.1" + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/typescript-estree": "npm:8.56.1" + "@typescript-eslint/visitor-keys": "npm:8.56.1" + debug: "npm:^4.4.3" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/b6aab1d82cc98a77aaae7637bf2934980104799793b3fd5b893065d930fe9b23cd6c2059d6f73fb454ea08f9e956e84fa940310d8435092a14be645a42062d94 + checksum: 10c0/61c9dab481e795b01835c00c9c7c845f1d7ea7faf3b8657fccee0f8658a65390cb5fe2b5230ae8c4241bd6e0c32aa9455a91989a492bd3bd6fec7c7d9339377a languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.51.0": - version: 8.51.0 - resolution: "@typescript-eslint/project-service@npm:8.51.0" +"@typescript-eslint/project-service@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/project-service@npm:8.56.1" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.51.0" - "@typescript-eslint/types": "npm:^8.51.0" - debug: "npm:^4.3.4" + "@typescript-eslint/tsconfig-utils": "npm:^8.56.1" + "@typescript-eslint/types": "npm:^8.56.1" + debug: "npm:^4.4.3" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/c6e6efbf79e126261e1742990b0872a34bbbe9931d99f0aabd12cb70a65a361e02d626db4b632dabee2b2c26b7e5b48344fc5a796c56438ae0788535e2bbe092 + checksum: 10c0/ca61cde575233bc79046d73ddd330d183fb3cbb941fddc31919336317cda39885c59296e2e5401b03d9325a64a629e842fd66865705ff0d85d83ee3ee40871e8 languageName: node linkType: hard @@ -5929,38 +6000,38 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.51.0": - version: 8.51.0 - resolution: "@typescript-eslint/scope-manager@npm:8.51.0" +"@typescript-eslint/scope-manager@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/scope-manager@npm:8.56.1" dependencies: - "@typescript-eslint/types": "npm:8.51.0" - "@typescript-eslint/visitor-keys": "npm:8.51.0" - checksum: 10c0/dd1e75fc13e6b1119954612d9e8ad3f2d91bc37dcde85fd00e959171aaf6c716c4c265c90c5accf24b5831bd3f48510b0775e5583085b8fa2ad5c37c8980ae1a + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/visitor-keys": "npm:8.56.1" + checksum: 10c0/89cc1af2635eee23f2aa2ff87c08f88f3ad972ebf67eaacdc604a4ef4178535682bad73fd086e6f3c542e4e5d874253349af10d58291d079cc29c6c7e9831de4 languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.51.0, @typescript-eslint/tsconfig-utils@npm:^8.51.0": - version: 8.51.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.51.0" +"@typescript-eslint/tsconfig-utils@npm:8.56.1, @typescript-eslint/tsconfig-utils@npm:^8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.56.1" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/46cab9a5342b4a8f8a1d05aaee4236c5262a540ad0bca1f0e8dad5d63ed1e634b88ce0c82a612976dab09861e21086fc995a368df0435ac43fb960e0b9e5cde2 + checksum: 10c0/d03b64d7ff19020beeefa493ae667c2e67a4547d25a3ecb9210a3a52afe980c093d772a91014bae699ee148bfb60cc659479e02bfc2946ea06954a8478ef1fe1 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.51.0": - version: 8.51.0 - resolution: "@typescript-eslint/type-utils@npm:8.51.0" +"@typescript-eslint/type-utils@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/type-utils@npm:8.56.1" dependencies: - "@typescript-eslint/types": "npm:8.51.0" - "@typescript-eslint/typescript-estree": "npm:8.51.0" - "@typescript-eslint/utils": "npm:8.51.0" - debug: "npm:^4.3.4" - ts-api-utils: "npm:^2.2.0" + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/typescript-estree": "npm:8.56.1" + "@typescript-eslint/utils": "npm:8.56.1" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.4.0" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/7c17214e54bc3a4fe4551d9251ffbac52e84ca46eeae840c0f981994b7cbcc837ef32a2b6d510b02d958a8f568df355e724d9c6938a206716271a1b0c00801b7 + checksum: 10c0/66517aed5059ef4a29605d06a510582f934d5789ae40ad673f1f0421f8aa13ec9ba7b8caab57ae9f270afacbf13ec5359cedfe74f21ae77e9a2364929f7e7cee languageName: node linkType: hard @@ -5978,10 +6049,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.51.0, @typescript-eslint/types@npm:^8.46.0, @typescript-eslint/types@npm:^8.51.0": - version: 8.51.0 - resolution: "@typescript-eslint/types@npm:8.51.0" - checksum: 10c0/eb3473d0bb71eb886438f35887b620ffadae7853b281752a40c73158aee644d136adeb82549be7d7c30f346fe888b2e979dff7e30e67b35377e8281018034529 +"@typescript-eslint/types@npm:8.56.1, @typescript-eslint/types@npm:^8.46.4, @typescript-eslint/types@npm:^8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/types@npm:8.56.1" + checksum: 10c0/e5a0318abddf0c4f98da3039cb10b3c0601c8601f7a9f7043630f0d622dabfe83a4cd833545ad3531fc846e46ca2874377277b392c2490dffec279d9242d827b languageName: node linkType: hard @@ -6021,22 +6092,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.51.0": - version: 8.51.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.51.0" +"@typescript-eslint/typescript-estree@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.56.1" dependencies: - "@typescript-eslint/project-service": "npm:8.51.0" - "@typescript-eslint/tsconfig-utils": "npm:8.51.0" - "@typescript-eslint/types": "npm:8.51.0" - "@typescript-eslint/visitor-keys": "npm:8.51.0" - debug: "npm:^4.3.4" - minimatch: "npm:^9.0.4" - semver: "npm:^7.6.0" + "@typescript-eslint/project-service": "npm:8.56.1" + "@typescript-eslint/tsconfig-utils": "npm:8.56.1" + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/visitor-keys": "npm:8.56.1" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" tinyglobby: "npm:^0.2.15" - ts-api-utils: "npm:^2.2.0" + ts-api-utils: "npm:^2.4.0" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/5386acc67298a6757681b6264c29a6b9304be7a188f11498bbaa82bb0a3095fd79394ad80d6520bdff3fa3093199f9a438246604ee3281b76f7ed574b7516854 + checksum: 10c0/92f4421dac41be289761200dc2ed85974fa451deacb09490ae1870a25b71b97218e609a90d4addba9ded5b2abdebc265c9db7f6e9ce6d29ed20e89b8487e9618 languageName: node linkType: hard @@ -6058,18 +6129,18 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.51.0": - version: 8.51.0 - resolution: "@typescript-eslint/utils@npm:8.51.0" +"@typescript-eslint/utils@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/utils@npm:8.56.1" dependencies: - "@eslint-community/eslint-utils": "npm:^4.7.0" - "@typescript-eslint/scope-manager": "npm:8.51.0" - "@typescript-eslint/types": "npm:8.51.0" - "@typescript-eslint/typescript-estree": "npm:8.51.0" + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.56.1" + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/typescript-estree": "npm:8.56.1" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/ffb8237cfb33a1998ae2812b136d42fb65e7497f185d46097d19e43112e41b3ef59f901ba679c2e5372ad3007026f6e5add3a3de0f2e75ce6896918713fa38a8 + checksum: 10c0/d9ffd9b2944a2c425e0532f71dc61e61d0a923d1a17733cf2777c2a4ae638307d12d44f63b33b6b3dc62f02f47db93ec49344ecefe17b76ee3e4fb0833325be3 languageName: node linkType: hard @@ -6108,13 +6179,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.51.0": - version: 8.51.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.51.0" +"@typescript-eslint/visitor-keys@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.56.1" dependencies: - "@typescript-eslint/types": "npm:8.51.0" - eslint-visitor-keys: "npm:^4.2.1" - checksum: 10c0/fce5603961cf336e71095f7599157de65e3182f61cbd6cab33a43551ee91485b4e9bf6cacc1b275cf6f3503b92f8568fe2267a45c82e60e386ee73db727a26ca + "@typescript-eslint/types": "npm:8.56.1" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/86d97905dec1af964cc177c185933d040449acf6006096497f2e0093c6a53eb92b3ac1db9eb40a5a2e8d91160f558c9734331a9280797f09f284c38978b22190 languageName: node linkType: hard @@ -6344,12 +6415,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.15.0": - version: 8.15.0 - resolution: "acorn@npm:8.15.0" +"acorn@npm:^8.16.0": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" bin: acorn: bin/acorn - checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec + checksum: 10c0/c9c52697227661b68d0debaf972222d4f622aa06b185824164e153438afa7b08273432ca43ea792cadb24dada1d46f6f6bb1ef8de9956979288cc1b96bf9914e languageName: node linkType: hard @@ -6744,39 +6815,39 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.4.14": - version: 0.4.14 - resolution: "babel-plugin-polyfill-corejs2@npm:0.4.14" +"babel-plugin-polyfill-corejs2@npm:^0.4.15": + version: 0.4.15 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.15" dependencies: - "@babel/compat-data": "npm:^7.27.7" - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" + "@babel/compat-data": "npm:^7.28.6" + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10c0/d74cba0600a6508e86d220bde7164eb528755d91be58020e5ea92ea7fbb12c9d8d2c29246525485adfe7f68ae02618ec428f9a589cac6cbedf53cc3972ad7fbe + checksum: 10c0/5e3ff853a5056bdc0816320523057b45d52c9ea01c847fd07886a4202b0c1324dc97eda4b777c98387927ff02d913fedbe9ba9943c0d4030714048e0b9e61682 languageName: node linkType: hard -"babel-plugin-polyfill-corejs3@npm:^0.13.0": - version: 0.13.0 - resolution: "babel-plugin-polyfill-corejs3@npm:0.13.0" +"babel-plugin-polyfill-corejs3@npm:^0.14.0": + version: 0.14.0 + resolution: "babel-plugin-polyfill-corejs3@npm:0.14.0" dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" - core-js-compat: "npm:^3.43.0" + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" + core-js-compat: "npm:^3.48.0" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10c0/5d8e228da425edc040d8c868486fd01ba10b0440f841156a30d9f8986f330f723e2ee61553c180929519563ef5b64acce2caac36a5a847f095d708dda5d8206d + checksum: 10c0/db7f530752a2bcb891c0dc80c3d025a48d49c78d41b0ad91cc853669460cd9e3107857a3667f645f0e25c2af9fc3d1e38d5b1c4e3e60aa22e7df9d68550712a4 languageName: node linkType: hard -"babel-plugin-polyfill-regenerator@npm:^0.6.5": - version: 0.6.5 - resolution: "babel-plugin-polyfill-regenerator@npm:0.6.5" +"babel-plugin-polyfill-regenerator@npm:^0.6.6": + version: 0.6.6 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.6" dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10c0/63aa8ed716df6a9277c6ab42b887858fa9f57a70cc1d0ae2b91bdf081e45d4502848cba306fb60b02f59f99b32fd02ff4753b373cac48ccdac9b7d19dd56f06d + checksum: 10c0/0ef91d8361c118e7b16d8592c053707325b8168638ea4636b76530c8bc6a1b5aac5c6ca5140e8f3fcdb634a7a2e636133e6b9ef70a75e6417a258a7fddc04bd7 languageName: node linkType: hard @@ -6797,6 +6868,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b + languageName: node + linkType: hard + "bare-events@npm:^2.2.0": version: 2.5.4 resolution: "bare-events@npm:2.5.4" @@ -6903,6 +6981,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.2": + version: 5.0.3 + resolution: "brace-expansion@npm:5.0.3" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10c0/e474d300e581ec56851b3863ff1cf18573170c6d06deb199ccbd03b2119c36975f6ce2abc7b770f5bebddc1ab022661a9fea9b4d56f33315d7bef54d8793869e + languageName: node + linkType: hard + "braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -7056,20 +7143,6 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.25.1": - version: 4.25.1 - resolution: "browserslist@npm:4.25.1" - dependencies: - caniuse-lite: "npm:^1.0.30001726" - electron-to-chromium: "npm:^1.5.173" - node-releases: "npm:^2.0.19" - update-browserslist-db: "npm:^1.1.3" - bin: - browserslist: cli.js - checksum: 10c0/acba5f0bdbd5e72dafae1e6ec79235b7bad305ed104e082ed07c34c38c7cb8ea1bc0f6be1496958c40482e40166084458fc3aee15111f15faa79212ad9081b2a - languageName: node - linkType: hard - "browserslist@npm:^4.28.1": version: 4.28.1 resolution: "browserslist@npm:4.28.1" @@ -7259,7 +7332,7 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001688, caniuse-lite@npm:^1.0.30001726": +"caniuse-lite@npm:^1.0.30001688": version: 1.0.30001757 resolution: "caniuse-lite@npm:1.0.30001757" checksum: 10c0/3ccb71fa2bf1f8c96ff1bf9b918b08806fed33307e20a3ce3259155fda131eaf96cfcd88d3d309c8fd7f8285cc71d89a3b93648a1c04814da31c301f98508d42 @@ -7637,12 +7710,12 @@ __metadata: languageName: node linkType: hard -"core-js-compat@npm:^3.43.0": - version: 3.44.0 - resolution: "core-js-compat@npm:3.44.0" +"core-js-compat@npm:^3.48.0": + version: 3.48.0 + resolution: "core-js-compat@npm:3.48.0" dependencies: - browserslist: "npm:^4.25.1" - checksum: 10c0/5de4b042b8bb232b8390be3079030de5c7354610f136ed3eb91310a44455a78df02cfcf49b2fd05d5a5aa2695460620abf1b400784715f7482ed4770d40a68b2 + browserslist: "npm:^4.28.1" + checksum: 10c0/7bb6522127928fff5d56c7050f379a034de85fe2d5c6e6925308090d4b51fb0cb88e0db99619c932ee84d8756d531bf851232948fe1ad18598cb1e7278e8db13 languageName: node linkType: hard @@ -8006,6 +8079,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.6.0": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa + languageName: node + linkType: hard + "deep-eql@npm:^5.0.1": version: 5.0.2 resolution: "deep-eql@npm:5.0.2" @@ -8270,13 +8350,6 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.173": - version: 1.5.178 - resolution: "electron-to-chromium@npm:1.5.178" - checksum: 10c0/2734c8ee211fb6c5b4ac55d5797cbf9882a37515c3f9403427b8a97d75413f9e08786d1f5d7aa7dfd433bd53b0ae97fb186bcdd5bb137978eb0fa6a436f07de4 - languageName: node - linkType: hard - "electron-to-chromium@npm:^1.5.263": version: 1.5.267 resolution: "electron-to-chromium@npm:1.5.267" @@ -8302,7 +8375,7 @@ __metadata: "@codecov/vite-plugin": "npm:^1.3.0" "@fontsource/inconsolata": "npm:^5.1.0" "@fontsource/inter": "npm:^5.1.0" - "@formatjs/intl-durationformat": "npm:^0.9.0" + "@formatjs/intl-durationformat": "npm:^0.10.0" "@formatjs/intl-segmenter": "npm:^11.7.3" "@livekit/components-core": "npm:^0.12.0" "@livekit/components-react": "npm:^2.0.0" @@ -8360,7 +8433,7 @@ __metadata: i18next-browser-languagedetector: "npm:^8.0.0" i18next-parser: "npm:^9.1.0" jsdom: "npm:^26.0.0" - knip: "npm:^5.27.2" + knip: "npm:5.82.1" livekit-client: "npm:^2.13.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" @@ -8377,7 +8450,7 @@ __metadata: qrcode: "npm:^1.5.4" react: "npm:19" react-dom: "npm:19" - react-i18next: "npm:^16.0.0 <16.1.0" + react-i18next: "npm:^16.0.0 <16.6.0" react-router-dom: "npm:^7.0.0" react-use-measure: "npm:^2.1.1" rxjs: "npm:^7.8.1" @@ -9013,17 +9086,17 @@ __metadata: linkType: hard "eslint-plugin-jsdoc@npm:^61.5.0": - version: 61.5.0 - resolution: "eslint-plugin-jsdoc@npm:61.5.0" + version: 61.7.1 + resolution: "eslint-plugin-jsdoc@npm:61.7.1" dependencies: - "@es-joy/jsdoccomment": "npm:~0.76.0" + "@es-joy/jsdoccomment": "npm:~0.78.0" "@es-joy/resolve.exports": "npm:1.2.0" are-docs-informative: "npm:^0.0.2" comment-parser: "npm:1.4.1" debug: "npm:^4.4.3" escape-string-regexp: "npm:^4.0.0" - espree: "npm:^10.4.0" - esquery: "npm:^1.6.0" + espree: "npm:^11.0.0" + esquery: "npm:^1.7.0" html-entities: "npm:^2.6.0" object-deep-merge: "npm:^2.0.0" parse-imports-exports: "npm:^0.2.4" @@ -9032,7 +9105,7 @@ __metadata: to-valid-identifier: "npm:^1.0.0" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10c0/fabb04f6efe58a167a0839d3c05676a76080c6e91d98a269fa768c1bfd835aa0ded5822d400da2874216177044d2d227ebe241d73e923f3fe1c08bafd19cfd3d + checksum: 10c0/d0904b923f68a4e9e6da156316a4e2a972445bf79118bde9618ad80b4ef5927fc2c9dd597b22b776742ef548d65914e75fca190ab3be942385f268a3b83c1087 languageName: node linkType: hard @@ -9204,10 +9277,10 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.2.1": - version: 4.2.1 - resolution: "eslint-visitor-keys@npm:4.2.1" - checksum: 10c0/fcd43999199d6740db26c58dbe0c2594623e31ca307e616ac05153c9272f12f1364f5a0b1917a8e962268fdecc6f3622c1c2908b4fcc2e047a106fe6de69dc43 +"eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678 languageName: node linkType: hard @@ -9270,14 +9343,14 @@ __metadata: languageName: node linkType: hard -"espree@npm:^10.4.0": - version: 10.4.0 - resolution: "espree@npm:10.4.0" +"espree@npm:^11.0.0": + version: 11.1.1 + resolution: "espree@npm:11.1.1" dependencies: - acorn: "npm:^8.15.0" + acorn: "npm:^8.16.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^4.2.1" - checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b + eslint-visitor-keys: "npm:^5.0.1" + checksum: 10c0/2feae74efdfb037b9e9fcb30506799845cf20900de5e441ed03e5c51aaa249f85ea5818ff177682acc0c9bfb4ac97e1965c238ee44ac7c305aab8747177bab69 languageName: node linkType: hard @@ -9301,6 +9374,15 @@ __metadata: languageName: node linkType: hard +"esquery@npm:^1.7.0": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" + dependencies: + estraverse: "npm:^5.1.0" + checksum: 10c0/77d5173db450b66f3bc685d11af4c90cffeedb340f34a39af96d43509a335ce39c894fd79233df32d38f5e4e219fa0f7076f6ec90bae8320170ba082c0db4793 + languageName: node + linkType: hard + "esrecurse@npm:^4.3.0": version: 4.3.0 resolution: "esrecurse@npm:4.3.0" @@ -10271,11 +10353,11 @@ __metadata: linkType: hard "i18next-browser-languagedetector@npm:^8.0.0": - version: 8.2.0 - resolution: "i18next-browser-languagedetector@npm:8.2.0" + version: 8.2.1 + resolution: "i18next-browser-languagedetector@npm:8.2.1" dependencies: "@babel/runtime": "npm:^7.23.2" - checksum: 10c0/4fcb6ec316e0fd4a10eee67a8d1e3d7e1407f14d5bed98978c50ed6f1853f5d559dc18ea7fd4b2de445ac0a4ed44df5b38f0b31b89b9ac883f99050d59ffec82 + checksum: 10c0/d200847a79b4cb2764ef59b33e5399085d4d56b2b038e884bb54fffe17953b467899142a6ef6e985592234f10049d14d06b80f2c56441ae80648c5a6717704f3 languageName: node linkType: hard @@ -10321,8 +10403,8 @@ __metadata: linkType: hard "i18next@npm:^25.0.0": - version: 25.7.3 - resolution: "i18next@npm:25.7.3" + version: 25.8.13 + resolution: "i18next@npm:25.8.13" dependencies: "@babel/runtime": "npm:^7.28.4" peerDependencies: @@ -10330,7 +10412,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/0b10452c26d6526bbfa8f0fb76241f5e17f0dc08d9b9cc9810bc3103047a3656ec6482b170a86a408a49178af5683a4b88b43986580c5f95f497d4afc9719088 + checksum: 10c0/12c661c2b58fe70445f8491b72f937eef28a5f9413f76bd178bbca92d4378d8436003c3bea1d5d760b8a69f809cbcef2ce389beffd9bc0434651134c6b37fecc languageName: node linkType: hard @@ -10357,10 +10439,10 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^7.0.0": - version: 7.0.4 - resolution: "ignore@npm:7.0.4" - checksum: 10c0/90e1f69ce352b9555caecd9cbfd07abe7626d312a6f90efbbb52c7edca6ea8df065d66303863b30154ab1502afb2da8bc59d5b04e1719a52ef75bbf675c488eb +"ignore@npm:^7.0.5": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: 10c0/ae00db89fe873064a093b8999fe4cc284b13ef2a178636211842cceb650b9c3e390d3339191acb145d81ed5379d2074840cf0c33a20bdbd6f32821f79eb4ad5d languageName: node linkType: hard @@ -10517,7 +10599,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.1": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -10975,10 +11057,10 @@ __metadata: languageName: node linkType: hard -"jsdoc-type-pratt-parser@npm:~6.10.0": - version: 6.10.0 - resolution: "jsdoc-type-pratt-parser@npm:6.10.0" - checksum: 10c0/8ea395df0cae0e41d4bdba5f8d81b8d3e467fe53d1e4182a5d4e653235a5f17d60ed137343d68dbc74fa10e767f1c58fb85b1f6d5489c2cf16fc7216cc6d3e1a +"jsdoc-type-pratt-parser@npm:~7.0.0": + version: 7.0.0 + resolution: "jsdoc-type-pratt-parser@npm:7.0.0" + checksum: 10c0/3ede53c80dddf940a51dcdc79e3923537650f6fb6e9001fc76023c2d5cb0195cc8b24b7eebf9b3f20a7bc00d5e6b7f70318f0b8cb5972f6aff884152e6698014 languageName: node linkType: hard @@ -11015,7 +11097,7 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:^3.0.2": +"jsesc@npm:^3.0.2, jsesc@npm:~3.1.0": version: 3.1.0 resolution: "jsesc@npm:3.1.0" bin: @@ -11150,9 +11232,9 @@ __metadata: languageName: node linkType: hard -"knip@npm:^5.27.2": - version: 5.79.0 - resolution: "knip@npm:5.79.0" +"knip@npm:5.82.1": + version: 5.82.1 + resolution: "knip@npm:5.82.1" dependencies: "@nodelib/fs.walk": "npm:^1.2.3" fast-glob: "npm:^3.3.3" @@ -11172,7 +11254,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10c0/dc3599247763912c0602621b83d125cba4e111d85ec5f01f9b65808a0091a60d7be85ed6cecc93d0afb39b895127231f7a68dd4c4bb7e210dd727b6ef9c1571d + checksum: 10c0/c3bfe898fe3103bb6a59ee2ba4297f05ea4d2db474571db89ae199ebbd74eafa5061d05b3bc2c75e4ec2322ba7ffee44493c76132d3d8991fae66ba742b9ccb4 languageName: node linkType: hard @@ -11561,6 +11643,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.2.2": + version: 10.2.3 + resolution: "minimatch@npm:10.2.3" + dependencies: + brace-expansion: "npm:^5.0.2" + checksum: 10c0/d9ae5f355e8bb77a42dd8c20b950141cec8773ef8716a2bb6df7a6840cc44a00ed828883884e4f1c7b5cb505fa06a17e3ea9ca2edb18fd1dec865ea7f9fcf0e5 + languageName: node + linkType: hard + "minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -12460,27 +12551,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.57.0": - version: 1.57.0 - resolution: "playwright-core@npm:1.57.0" +"playwright-core@npm:1.58.2": + version: 1.58.2 + resolution: "playwright-core@npm:1.58.2" bin: playwright-core: cli.js - checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9 + checksum: 10c0/5aa15b2b764e6ffe738293a09081a6f7023847a0dbf4cd05fe10eed2e25450d321baf7482f938f2d2eb330291e197fa23e57b29a5b552b89927ceb791266225b languageName: node linkType: hard -"playwright@npm:1.57.0": - version: 1.57.0 - resolution: "playwright@npm:1.57.0" +"playwright@npm:1.58.2": + version: 1.58.2 + resolution: "playwright@npm:1.58.2" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.57.0" + playwright-core: "npm:1.58.2" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899 + checksum: 10c0/d060d9b7cc124bd8b5dffebaab5e84f6b34654a553758fe7b19cc598dfbee93f6ecfbdc1832b40a6380ae04eade86ef3285ba03aa0b136799e83402246dc0727 languageName: node linkType: hard @@ -12765,8 +12856,8 @@ __metadata: linkType: hard "postcss-preset-env@npm:^10.0.0": - version: 10.6.0 - resolution: "postcss-preset-env@npm:10.6.0" + version: 10.6.1 + resolution: "postcss-preset-env@npm:10.6.1" dependencies: "@csstools/postcss-alpha-function": "npm:^1.0.1" "@csstools/postcss-cascade-layers": "npm:^5.0.2" @@ -12793,7 +12884,7 @@ __metadata: "@csstools/postcss-media-minmax": "npm:^2.0.9" "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^3.0.5" "@csstools/postcss-nested-calc": "npm:^4.0.0" - "@csstools/postcss-normalize-display-values": "npm:^4.0.0" + "@csstools/postcss-normalize-display-values": "npm:^4.0.1" "@csstools/postcss-oklab-function": "npm:^4.0.12" "@csstools/postcss-position-area-property": "npm:^1.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" @@ -12841,7 +12932,7 @@ __metadata: postcss-selector-not: "npm:^8.0.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/61162c9d675004db842d58829605c3c9ee81ed1a15684793a419b94c2c28e3be2ff9a7373f0996a1a255caf208d8f3d5dd907e61af1bbb0c7634e3215e87fc56 + checksum: 10c0/e8da96f208918ebc0dc9acc8ba8961a92569f1d130b29abe25adaf7dbd56ef29fc6f778b75964c80fe7f3469012c763ea9447e5c2f559a002a155bc0462cce35 languageName: node linkType: hard @@ -12930,11 +13021,11 @@ __metadata: linkType: hard "prettier@npm:^3.0.0": - version: 3.7.4 - resolution: "prettier@npm:3.7.4" + version: 3.8.1 + resolution: "prettier@npm:3.8.1" bin: prettier: bin/prettier.cjs - checksum: 10c0/9675d2cd08eacb1faf1d1a2dbfe24bfab6a912b059fc9defdb380a408893d88213e794a40a2700bd29b140eb3172e0b07c852853f6e22f16f3374659a1a13389 + checksum: 10c0/33169b594009e48f570471271be7eac7cdcf88a209eed39ac3b8d6d78984039bfa9132f82b7e6ba3b06711f3bfe0222a62a1bfb87c43f50c25a83df1b78a2c42 languageName: node linkType: hard @@ -13117,24 +13208,25 @@ __metadata: linkType: hard "react-dom@npm:19": - version: 19.2.3 - resolution: "react-dom@npm:19.2.3" + version: 19.2.4 + resolution: "react-dom@npm:19.2.4" dependencies: scheduler: "npm:^0.27.0" peerDependencies: - react: ^19.2.3 - checksum: 10c0/dc43f7ede06f46f3acc16ee83107c925530de9b91d1d0b3824583814746ff4c498ea64fd65cd83aba363205268adff52e2827c582634ae7b15069deaeabc4892 + react: ^19.2.4 + checksum: 10c0/f0c63f1794dedb154136d4d0f59af00b41907f4859571c155940296808f4b94bf9c0c20633db75b5b2112ec13d8d7dd4f9bf57362ed48782f317b11d05a44f35 languageName: node linkType: hard -"react-i18next@npm:^16.0.0 <16.1.0": - version: 16.0.1 - resolution: "react-i18next@npm:16.0.1" +"react-i18next@npm:^16.0.0 <16.6.0": + version: 16.5.4 + resolution: "react-i18next@npm:16.5.4" dependencies: - "@babel/runtime": "npm:^7.27.6" + "@babel/runtime": "npm:^7.28.4" html-parse-stringify: "npm:^3.0.1" + use-sync-external-store: "npm:^1.6.0" peerDependencies: - i18next: ">= 25.5.2" + i18next: ">= 25.6.2" react: ">= 16.8.0" typescript: ^5 peerDependenciesMeta: @@ -13144,7 +13236,7 @@ __metadata: optional: true typescript: optional: true - checksum: 10c0/8fcd8dea9bd083aac37acf569478872980216d2f2d85eff18444316a407832f1d3f589d1d0e587c6fe709d43423f5ec82c2a96f52cc2999ddd2180fa3de20ace + checksum: 10c0/41d0b76873addfa3abe0c6b8a10a796e01f205f3636bc2d090d0078b42222f2949c4303f18d7a80cc26cf1298918cb6220d96e39ae2b8644abfdbec3bb504b37 languageName: node linkType: hard @@ -13224,20 +13316,20 @@ __metadata: linkType: hard "react-router-dom@npm:^7.0.0": - version: 7.11.0 - resolution: "react-router-dom@npm:7.11.0" + version: 7.13.1 + resolution: "react-router-dom@npm:7.13.1" dependencies: - react-router: "npm:7.11.0" + react-router: "npm:7.13.1" peerDependencies: react: ">=18" react-dom: ">=18" - checksum: 10c0/0e8061fe0ef7915cc411dd92f5f41109f6343b6abef36571b08ff231365bf61f52364bea128d1c964e9b8eb19426c9bd21923df0b3e1bb993d21bd2b7440fb49 + checksum: 10c0/2b8ed9dc753f1f7be599a53a00900df04e2b4d1186b0a4d63004eebb2250cd78cd6837ff15fcada5f88d53ad127fff0d1de31468715dcd6dd79dad8cfa8414e9 languageName: node linkType: hard -"react-router@npm:7.11.0": - version: 7.11.0 - resolution: "react-router@npm:7.11.0" +"react-router@npm:7.13.1": + version: 7.13.1 + resolution: "react-router@npm:7.13.1" dependencies: cookie: "npm:^1.0.1" set-cookie-parser: "npm:^2.6.0" @@ -13247,7 +13339,7 @@ __metadata: peerDependenciesMeta: react-dom: optional: true - checksum: 10c0/eb3693d63d1c52221a3449de5db170e2fa9e00536b011998b17f8a277f8b5e89b752d104dbbeb4ee3d474f8e4570167db00293b4510f63277e5e6658c5dab22b + checksum: 10c0/a64c645cede74251f21483fbfad740b36dc5133522d6f53f12317a873a22865fce659d4c2377d5e19c912f85c7b12b88224a2c70d8f70c082496b569cc4abc31 languageName: node linkType: hard @@ -13281,9 +13373,9 @@ __metadata: linkType: hard "react@npm:19": - version: 19.2.3 - resolution: "react@npm:19.2.3" - checksum: 10c0/094220b3ba3a76c1b668f972ace1dd15509b157aead1b40391d1c8e657e720c201d9719537375eff08f5e0514748c0319063392a6f000e31303aafc4471f1436 + version: 19.2.4 + resolution: "react@npm:19.2.4" + checksum: 10c0/cd2c9ff67a720799cc3b38a516009986f7fc4cb8d3e15716c6211cf098d1357ee3e348ab05ad0600042bbb0fd888530ba92e329198c92eafa0994f5213396596 languageName: node linkType: hard @@ -13387,6 +13479,15 @@ __metadata: languageName: node linkType: hard +"regenerate-unicode-properties@npm:^10.2.2": + version: 10.2.2 + resolution: "regenerate-unicode-properties@npm:10.2.2" + dependencies: + regenerate: "npm:^1.4.2" + checksum: 10c0/66a1d6a1dbacdfc49afd88f20b2319a4c33cee56d245163e4d8f5f283e0f45d1085a78f7f7406dd19ea3a5dd7a7799cd020cd817c97464a7507f9d10fbdce87c + languageName: node + linkType: hard + "regenerate@npm:^1.4.2": version: 1.4.2 resolution: "regenerate@npm:1.4.2" @@ -13452,6 +13553,20 @@ __metadata: languageName: node linkType: hard +"regexpu-core@npm:^6.3.1": + version: 6.4.0 + resolution: "regexpu-core@npm:6.4.0" + dependencies: + regenerate: "npm:^1.4.2" + regenerate-unicode-properties: "npm:^10.2.2" + regjsgen: "npm:^0.8.0" + regjsparser: "npm:^0.13.0" + unicode-match-property-ecmascript: "npm:^2.0.0" + unicode-match-property-value-ecmascript: "npm:^2.2.1" + checksum: 10c0/1eed9783c023dd06fb1f3ce4b6e3fdf0bc1e30cb036f30aeb2019b351e5e0b74355b40462282ea5db092c79a79331c374c7e9897e44a5ca4509e9f0b570263de + languageName: node + linkType: hard + "regjsgen@npm:^0.8.0": version: 0.8.0 resolution: "regjsgen@npm:0.8.0" @@ -13481,6 +13596,17 @@ __metadata: languageName: node linkType: hard +"regjsparser@npm:^0.13.0": + version: 0.13.0 + resolution: "regjsparser@npm:0.13.0" + dependencies: + jsesc: "npm:~3.1.0" + bin: + regjsparser: bin/parser + checksum: 10c0/4702f85cda09f67747c1b2fb673a0f0e5d1ba39d55f177632265a0be471ba59e3f320623f411649141f752b126b8126eac3ff4c62d317921e430b0472bfc6071 + languageName: node + linkType: hard + "relateurl@npm:^0.2.7": version: 0.2.7 resolution: "relateurl@npm:0.2.7" @@ -13559,7 +13685,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.17.0": +"resolve@npm:^1.17.0, resolve@npm:^1.22.11": version: 1.22.11 resolution: "resolve@npm:1.22.11" dependencies: @@ -13572,19 +13698,6 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.22.10": - version: 1.22.10 - resolution: "resolve@npm:1.22.10" - dependencies: - is-core-module: "npm:^2.16.0" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10c0/8967e1f4e2cc40f79b7e080b4582b9a8c5ee36ffb46041dccb20e6461161adf69f843b43067b4a375de926a2cd669157e29a29578191def399dd5ef89a1b5203 - languageName: node - linkType: hard - "resolve@npm:^2.0.0-next.5": version: 2.0.0-next.5 resolution: "resolve@npm:2.0.0-next.5" @@ -13611,7 +13724,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.17.0#optional!builtin": +"resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin": version: 1.22.11 resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin::version=1.22.11&hash=c3c19d" dependencies: @@ -13624,19 +13737,6 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.22.10#optional!builtin": - version: 1.22.10 - resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" - dependencies: - is-core-module: "npm:^2.16.0" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10c0/52a4e505bbfc7925ac8f4cd91fd8c4e096b6a89728b9f46861d3b405ac9a1ccf4dcbf8befb4e89a2e11370dacd0160918163885cbc669369590f2f31f4c58939 - languageName: node - linkType: hard - "resolve@patch:resolve@npm%3A^2.0.0-next.5#optional!builtin": version: 2.0.0-next.5 resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#optional!builtin::version=2.0.0-next.5&hash=c3c19d" @@ -13897,8 +13997,8 @@ __metadata: linkType: hard "sass@npm:^1.42.1": - version: 1.97.1 - resolution: "sass@npm:1.97.1" + version: 1.97.3 + resolution: "sass@npm:1.97.3" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" @@ -13909,7 +14009,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: 10c0/c389d5d6405869b49fa2291e8328500fe7936f3b72136bc2c338bee6e7fec936bb9a48d77a1310dea66aa4669ba74ae6b82a112eb32521b9b36d740138a39ea0 + checksum: 10c0/67f6b5d220f20c1c23a8b16dda5fd1c5d119ad5caf8195b185d553b5b239fb188a3787f04fc00171c62515f2c4e5e0eb5ad4992a80f8543428556883c1240ba3 languageName: node linkType: hard @@ -14844,7 +14944,7 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^2.2.0": +"ts-api-utils@npm:^2.4.0": version: 2.4.0 resolution: "ts-api-utils@npm:2.4.0" peerDependencies: @@ -14872,7 +14972,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -15033,7 +15133,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.4": +"typescript@npm:^5.0.4, typescript@npm:^5.8.3": version: 5.9.3 resolution: "typescript@npm:5.9.3" bin: @@ -15043,17 +15143,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.8.3": - version: 5.8.3 - resolution: "typescript@npm:5.8.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin": +"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": version: 5.9.3 resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: @@ -15063,16 +15153,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": - version: 5.8.3 - resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb - languageName: node - linkType: hard - "unbox-primitive@npm:^1.1.0": version: 1.1.0 resolution: "unbox-primitive@npm:1.1.0" @@ -15102,6 +15182,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d + languageName: node + linkType: hard + "undici@npm:^5.25.4": version: 5.29.0 resolution: "undici@npm:5.29.0" @@ -15149,6 +15236,13 @@ __metadata: languageName: node linkType: hard +"unicode-match-property-value-ecmascript@npm:^2.2.1": + version: 2.2.1 + resolution: "unicode-match-property-value-ecmascript@npm:2.2.1" + checksum: 10c0/93acd1ad9496b600e5379d1aaca154cf551c5d6d4a0aefaf0984fc2e6288e99220adbeb82c935cde461457fb6af0264a1774b8dfd4d9a9e31548df3352a4194d + languageName: node + linkType: hard + "unicode-property-aliases-ecmascript@npm:^2.0.0": version: 2.1.0 resolution: "unicode-property-aliases-ecmascript@npm:2.1.0" @@ -15224,7 +15318,7 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.1.1, update-browserslist-db@npm:^1.1.3": +"update-browserslist-db@npm:^1.1.1": version: 1.1.3 resolution: "update-browserslist-db@npm:1.1.3" dependencies: @@ -15302,6 +15396,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.6.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b + languageName: node + linkType: hard + "usehooks-ts@npm:3.1.1": version: 3.1.1 resolution: "usehooks-ts@npm:3.1.1" @@ -15505,8 +15608,8 @@ __metadata: linkType: hard "vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0, vite@npm:^7.0.0": - version: 7.3.0 - resolution: "vite@npm:7.3.0" + version: 7.3.1 + resolution: "vite@npm:7.3.1" dependencies: esbuild: "npm:^0.27.0" fdir: "npm:^6.5.0" @@ -15555,7 +15658,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/0457c196cdd5761ec351c0f353945430fbad330e615b9eeab729c8ae163334f18acdc1d9cd7d9d673dbf111f07f6e4f0b25d4ac32360e65b4a6df9991046f3ff + checksum: 10c0/5c7548f5f43a23533e53324304db4ad85f1896b1bfd3ee32ae9b866bac2933782c77b350eb2b52a02c625c8ad1ddd4c000df077419410650c982cd97fde8d014 languageName: node linkType: hard