From e5117b962c14906211194daa9b42caf434dcb0bd Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Nov 2024 12:51:22 -0500 Subject: [PATCH 01/19] Enable audio output support on Firefox --- src/livekit/MediaDevicesContext.tsx | 18 +++--------------- src/settings/SettingsModal.tsx | 11 ++++------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index d405eec0..334eb2ec 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -26,7 +26,6 @@ import { videoInput as videoInputSetting, Setting, } from "../settings/settings"; -import { isFirefox } from "../Platform"; export interface MediaDevice { available: MediaDeviceInfo[]; @@ -61,7 +60,6 @@ function useMediaDevice( kind: MediaDeviceKind, setting: Setting, usingNames: boolean, - alwaysDefault: boolean = false, ): MediaDevice { // Make sure we don't needlessly reset to a device observer without names, // once permissions are already given @@ -89,7 +87,7 @@ function useMediaDevice( return useMemo(() => { let selectedId: string | undefined = undefined; - if (!alwaysDefault && available) { + if (available) { // If the preferred device is available, use it. Or if every available // device ID is falsy, the browser is probably just being paranoid about // fingerprinting and we should still try using the preferred device. @@ -112,7 +110,7 @@ function useMediaDevice( selectedId, select, }; - }, [available, preferredId, select, alwaysDefault]); + }, [available, preferredId, select]); } const deviceStub: MediaDevice = { @@ -139,15 +137,6 @@ export const MediaDevicesProvider: FC = ({ children }) => { const [numCallersUsingNames, setNumCallersUsingNames] = useState(0); const usingNames = numCallersUsingNames > 0; - // Setting the audio device to something other than 'undefined' breaks echo-cancellation - // and even can introduce multiple different output devices for one call. - const alwaysUseDefaultAudio = isFirefox(); - - // On FF we dont need to query the names - // (call enumerateDevices + create meadia stream to trigger permissions) - // for ouput devices because the selector wont be shown on FF. - const useOutputNames = usingNames && !isFirefox(); - const audioInput = useMediaDevice( "audioinput", audioInputSetting, @@ -156,8 +145,7 @@ export const MediaDevicesProvider: FC = ({ children }) => { const audioOutput = useMediaDevice( "audiooutput", audioOutputSetting, - useOutputNames, - alwaysUseDefaultAudio, + usingNames, ); const videoInput = useMediaDevice( "videoinput", diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 78afc2c5..79545f1d 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -29,7 +29,6 @@ import { useOptInAnalytics, soundEffectVolumeSetting, } from "./settings"; -import { isFirefox } from "../Platform"; import { PreferencesSettingsTab } from "./PreferencesSettingsTab"; import { Slider } from "../Slider"; import { DeviceSelection } from "./DeviceSelection"; @@ -96,12 +95,10 @@ export const SettingsModal: FC = ({ devices={devices.audioInput} caption={t("common.microphone")} /> - {!isFirefox() && ( - - )} +

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

From f249b7d46386aee15505a4c1f3a78f0479955c48 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Nov 2024 14:43:30 -0500 Subject: [PATCH 02/19] Create a virtual default audio output Managing your audio output manually is kind of cumbersome; Chrome creates a default audio output for us, but now that audio outputs are enabled on Firefox as well, I find it necessary for a good user experience that there always be a way to set it to "whatever the default is". --- locales/en-GB/app.json | 14 ++-- src/livekit/MediaDevicesContext.tsx | 105 ++++++++++++++++++---------- src/livekit/useLiveKit.ts | 6 +- src/room/MuteStates.ts | 4 +- src/settings/DeviceSelection.tsx | 31 +++++--- src/settings/SettingsModal.tsx | 11 ++- 6 files changed, 109 insertions(+), 62 deletions(-) diff --git a/locales/en-GB/app.json b/locales/en-GB/app.json index 0b9142d2..d71b8c9c 100644 --- a/locales/en-GB/app.json +++ b/locales/en-GB/app.json @@ -48,13 +48,11 @@ "audio": "Audio", "avatar": "Avatar", "back": "Back", - "camera": "Camera", "display_name": "Display name", "encrypted": "Encrypted", "error": "Error", "home": "Home", "loading": "Loading…", - "microphone": "Microphone", "next": "Next", "options": "Options", "password": "Password", @@ -149,6 +147,15 @@ "developer_settings_label": "Developer Settings", "developer_settings_label_description": "Expose developer settings in the settings window.", "developer_tab_title": "Developer", + "devices": { + "camera": "Camera", + "camera_numbered": "Camera {{n}}", + "default": "Default", + "microphone": "Microphone", + "microphone_numbered": "Microphone {{n}}", + "speaker": "Speaker", + "speaker_numbered": "Speaker {{n}}" + }, "duplicate_tiles_label": "Number of additional tile copies per participant", "feedback_tab_body": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.", "feedback_tab_description_label": "Your feedback", @@ -168,8 +175,7 @@ "preferences_tab_body": "Here you can configure extra options for an improved experience", "preferences_tab_h4": "Preferences", "preferences_tab_show_hand_raised_timer_description": "Show a timer when a participant raises their hand", - "preferences_tab_show_hand_raised_timer_label": "Show hand raise duration", - "speaker_device_selection_label": "Speaker" + "preferences_tab_show_hand_raised_timer_label": "Show hand raise duration" }, "star_rating_input_label_one": "{{count}} stars", "star_rating_input_label_other": "{{count}} stars", diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 334eb2ec..5382b331 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -16,7 +16,8 @@ import { useState, } from "react"; import { createMediaDeviceObserver } from "@livekit/components-core"; -import { Observable } from "rxjs"; +import { startWith } from "rxjs"; +import { useObservableEagerState } from "observable-hooks"; import { logger } from "matrix-js-sdk/src/logger"; import { @@ -27,9 +28,24 @@ import { Setting, } from "../settings/settings"; +export type DeviceLabel = + | { type: "name"; name: string } + | { type: "number"; number: number } + | { type: "default" }; + export interface MediaDevice { - available: MediaDeviceInfo[]; + /** + * A map from available device IDs to labels. + */ + available: Map; selectedId: string | undefined; + /** + * The group ID of the selected device. + */ + // This is exposed sort of ad-hoc because it's only needed for knowing when to + // restart the tracks of default input devices, and ideally this behavior + // would be encapsulated somehow… + selectedGroupId: string | undefined; select: (deviceId: string) => void; } @@ -41,21 +57,6 @@ export interface MediaDevices { stopUsingDeviceNames: () => void; } -// Cargo-culted from @livekit/components-react -function useObservableState( - observable: Observable | undefined, - startWith: T, -): T { - const [state, setState] = useState(startWith); - useEffect(() => { - // observable state doesn't run in SSR - if (typeof window === "undefined" || !observable) return; - const subscription = observable.subscribe(setState); - return (): void => subscription.unsubscribe(); - }, [observable]); - return state; -} - function useMediaDevice( kind: MediaDeviceKind, setting: Setting, @@ -79,43 +80,73 @@ function useMediaDevice( kind, () => logger.error("Error creating MediaDeviceObserver"), requestPermissions, - ), + ).pipe(startWith([])), [kind, requestPermissions], ); - const available = useObservableState(deviceObserver, []); - const [preferredId, select] = useSetting(setting); + const availableRaw = useObservableEagerState(deviceObserver); + const available = useMemo(() => { + // Sometimes browsers (particularly Firefox) can return multiple device + // entries for the exact same device ID; using a map deduplicates them + let available = new Map( + availableRaw.map((d, i) => [ + d.deviceId, + d.label + ? { type: "name", name: d.label } + : { type: "number", number: i + 1 }, + ]), + ); + // Create a virtual default audio output for browsers that don't have one. + // Its device ID must be the empty string because that's what setSinkId + // recognizes. + if ( + kind === "audiooutput" && + available.size && + !available.has("") && + !available.has("default") + ) + available = new Map([["", { type: "default" }], ...available]); + // Note: creating virtual default input devices would be another problem + // entirely, because requesting a media stream from deviceId "" won't + // automatically track the default device. + return available; + }, [kind, availableRaw]); - return useMemo(() => { - let selectedId: string | undefined = undefined; - if (available) { + const [preferredId, select] = useSetting(setting); + const selectedId = useMemo(() => { + if (available.size) { // If the preferred device is available, use it. Or if every available // device ID is falsy, the browser is probably just being paranoid about // fingerprinting and we should still try using the preferred device. // Worst case it is not available and the browser will gracefully fall // back to some other device for us when requesting the media stream. // Otherwise, select the first available device. - selectedId = - available.some((d) => d.deviceId === preferredId) || - available.every((d) => d.deviceId === "") - ? preferredId - : available.at(0)?.deviceId; + return (preferredId !== undefined && available.has(preferredId)) || + (available.size === 1 && available.has("")) + ? preferredId + : available.keys().next().value; } + return undefined; + }, [available, preferredId]); + const selectedGroupId = useMemo( + () => availableRaw.find((d) => d.deviceId === selectedId)?.groupId, + [availableRaw, selectedId], + ); - return { - available: available - ? // Sometimes browsers (particularly Firefox) can return multiple - // device entries for the exact same device ID; deduplicate them - [...new Map(available.map((d) => [d.deviceId, d])).values()] - : [], + return useMemo( + () => ({ + available, selectedId, + selectedGroupId, select, - }; - }, [available, preferredId, select]); + }), + [available, selectedId, selectedGroupId, select], + ); } const deviceStub: MediaDevice = { - available: [], + available: new Map(), selectedId: undefined, + selectedGroupId: undefined, select: () => {}, }; const devicesStub: MediaDevices = { diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 4645fab7..458ecaa0 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -290,18 +290,14 @@ export function useLiveKit( room.localParticipant.audioTrackPublications.values(), ).find((d) => d.source === Track.Source.Microphone)?.track; - const defaultDevice = device.available.find( - (d) => d.deviceId === "default", - ); if ( - defaultDevice && activeMicTrack && // only restart if the stream is still running: LiveKit will detect // when a track stops & restart appropriately, so this is not our job. // Plus, we need to avoid restarting again if the track is already in // the process of being restarted. activeMicTrack.mediaStreamTrack.readyState !== "ended" && - defaultDevice.groupId !== + device.selectedGroupId !== activeMicTrack.mediaStreamTrack.getSettings().groupId ) { // It's different, so restart the track, ie. cause Livekit to do another diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 261be59e..5fcadc90 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -54,12 +54,12 @@ function useMuteState( ): MuteState { const [enabled, setEnabled] = useReactiveState( (prev) => - device.available.length > 0 ? (prev ?? enabledByDefault()) : undefined, + device.available.size > 0 ? (prev ?? enabledByDefault()) : undefined, [device], ); return useMemo( () => - device.available.length === 0 + device.available.size === 0 ? deviceUnavailable : { enabled: enabled ?? false, diff --git a/src/settings/DeviceSelection.tsx b/src/settings/DeviceSelection.tsx index 005973a0..c4020822 100644 --- a/src/settings/DeviceSelection.tsx +++ b/src/settings/DeviceSelection.tsx @@ -13,16 +13,23 @@ import { RadioControl, Separator, } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; import { MediaDevice } from "../livekit/MediaDevicesContext"; import styles from "./DeviceSelection.module.css"; interface Props { devices: MediaDevice; - caption: string; + title: string; + numberedLabel: (number: number) => string; } -export const DeviceSelection: FC = ({ devices, caption }) => { +export const DeviceSelection: FC = ({ + devices, + title, + numberedLabel, +}) => { + const { t } = useTranslation(); const groupId = useId(); const onChange = useCallback( (e: ChangeEvent) => { @@ -31,7 +38,7 @@ export const DeviceSelection: FC = ({ devices, caption }) => { [devices], ); - if (devices.available.length == 0) return null; + if (devices.available.size == 0) return null; return (
@@ -42,26 +49,28 @@ export const DeviceSelection: FC = ({ devices, caption }) => { as="h4" className={styles.title} > - {caption} + {title}
- {devices.available.map(({ deviceId, label }, index) => ( + {[...devices.available].map(([id, label]) => ( } > ))} diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 79545f1d..3ac980ca 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -93,11 +93,15 @@ export const SettingsModal: FC = ({
+ t("settings.devices.microphone_numbered", { n }) + } /> t("settings.devices.speaker_numbered", { n })} />
@@ -123,7 +127,8 @@ export const SettingsModal: FC = ({ t("settings.devices.camera_numbered", { n })} /> ), From 00056a7cd9ce4f691c36d455416b1aca964b9944 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 12 Dec 2024 17:32:13 -0500 Subject: [PATCH 03/19] Determine which tiles are on screen in a more stable manner Instead of tracking for each individual tile whether it's visible, just track the total number of tiles that appear on screen. This ought to make the whole thing a lot less dynamic, which is crucial given that our UI renders asynchronously and RxJS doesn't really support cyclic dependencies in any rigorous way. In particular this ought to make the following kind of situation impossible: 1. There 3 tiles, ABC. A and B are on screen. 2. Now C becomes important. The requested order is now CAB. 3. To reduce the size of the layout shift, the algorithm selects to swap just B and C in the original order, giving ACB. However, the UI is blocked and doesn't render this order yet. 4. For whatever reason, a spurious update of the importance algorithm occurs. It once again requests CAB. 5. Now because the UI was blocked, the layout still thinks that A and B are on screen (rather than A and C). It thinks that C is some weird island of "off-screen territory" in the middle of the tile order. This confuses it into swapping A and C rather than keeping the layout stable. The reality is that whenever we think N tiles are visible on screen, we're always referring to the first N tiles in the grid. It's best if the code reflects this assumption. --- src/grid/Grid.tsx | 80 ++++++++++++++------------- src/grid/GridLayout.tsx | 11 +--- src/grid/OneOnOneLayout.tsx | 2 - src/grid/SpotlightExpandedLayout.tsx | 1 - src/grid/SpotlightLandscapeLayout.tsx | 11 +--- src/grid/SpotlightPortraitLayout.tsx | 11 +--- src/state/CallViewModel.test.ts | 5 +- src/state/CallViewModel.ts | 65 ++++++++++------------ src/state/GridLikeLayout.ts | 5 +- src/state/OneOnOneLayout.ts | 4 +- src/state/PipLayout.ts | 4 +- src/state/SpotlightExpandedLayout.ts | 4 +- src/state/TileStore.ts | 18 +++--- src/state/TileViewModel.ts | 10 +--- 14 files changed, 96 insertions(+), 135 deletions(-) diff --git a/src/grid/Grid.tsx b/src/grid/Grid.tsx index 411d9d08..268d4352 100644 --- a/src/grid/Grid.tsx +++ b/src/grid/Grid.tsx @@ -24,7 +24,6 @@ import { createContext, forwardRef, memo, - useCallback, useContext, useEffect, useMemo, @@ -54,7 +53,6 @@ interface Tile { id: string; model: Model; onDrag: DragCallback | undefined; - setVisible: (visible: boolean) => void; } type PlacedTile = Tile & Rect; @@ -88,7 +86,6 @@ interface SlotProps extends Omit, "onDrag"> { id: string; model: Model; onDrag?: DragCallback; - onVisibilityChange?: (visible: boolean) => void; style?: CSSProperties; className?: string; } @@ -115,24 +112,47 @@ function offset(element: HTMLElement, relativeTo: Element): Offset { } } +export type VisibleTilesCallback = (visibleTiles: number) => void; + interface LayoutContext { setGeneration: Dispatch>; + setVisibleTilesCallback: Dispatch< + SetStateAction + >; } const LayoutContext = createContext(null); +function useLayoutContext(): LayoutContext { + const context = useContext(LayoutContext); + if (context === null) + throw new Error("useUpdateLayout called outside a Grid layout context"); + return context; +} + /** * Enables Grid to react to layout changes. You must call this in your Layout * component or else Grid will not be reactive. */ export function useUpdateLayout(): void { - const context = useContext(LayoutContext); - if (context === null) - throw new Error("useUpdateLayout called outside a Grid layout context"); - + const { setGeneration } = useLayoutContext(); // On every render, tell Grid that the layout may have changed - useEffect(() => - context.setGeneration((prev) => (prev === null ? 0 : prev + 1)), + useEffect(() => setGeneration((prev) => (prev === null ? 0 : prev + 1))); +} + +/** + * Asks Grid to call a callback whenever the number of visible tiles may have + * changed. + */ +export function useVisibleTiles(callback: VisibleTilesCallback): void { + const { setVisibleTilesCallback } = useLayoutContext(); + useEffect( + () => setVisibleTilesCallback(() => callback), + [callback, setVisibleTilesCallback], + ); + useEffect( + () => (): void => setVisibleTilesCallback(null), + [setVisibleTilesCallback], ); } @@ -245,39 +265,20 @@ export function Grid< const windowHeight = useObservableEagerState(windowHeightObservable); const [layoutRoot, setLayoutRoot] = useState(null); const [generation, setGeneration] = useState(null); + const [visibleTilesCallback, setVisibleTilesCallback] = + useState(null); const tiles = useInitial(() => new Map>()); const prefersReducedMotion = usePrefersReducedMotion(); const Slot: FC> = useMemo( () => - function Slot({ - id, - model, - onDrag, - onVisibilityChange, - style, - className, - ...props - }) { + function Slot({ id, model, onDrag, style, className, ...props }) { const ref = useRef(null); - const prevVisible = useRef(null); - const setVisible = useCallback( - (visible: boolean) => { - if ( - onVisibilityChange !== undefined && - visible !== prevVisible.current - ) { - onVisibilityChange(visible); - prevVisible.current = visible; - } - }, - [onVisibilityChange], - ); useEffect(() => { - tiles.set(id, { id, model, onDrag, setVisible }); + tiles.set(id, { id, model, onDrag }); return (): void => void tiles.delete(id); - }, [id, model, onDrag, setVisible]); + }, [id, model, onDrag]); return (
({ setGeneration }), []); + const context: LayoutContext = useMemo( + () => ({ setGeneration, setVisibleTilesCallback }), + [setVisibleTilesCallback], + ); // Combine the tile definitions and slots together to create placed tiles const placedTiles = useMemo(() => { @@ -342,9 +346,11 @@ export function Grid< ); useEffect(() => { - for (const tile of placedTiles) - tile.setVisible(tile.y + tile.height <= visibleHeight); - }, [placedTiles, visibleHeight]); + visibleTilesCallback?.( + placedTiles.filter((tile) => tile.y + tile.height <= visibleHeight) + .length, + ); + }, [placedTiles, visibleTilesCallback, visibleHeight]); // Drag state is stored in a ref rather than component state, because we use // react-spring's imperative API during gestures to improve responsiveness diff --git a/src/grid/GridLayout.tsx b/src/grid/GridLayout.tsx index 45aecd85..f4a29379 100644 --- a/src/grid/GridLayout.tsx +++ b/src/grid/GridLayout.tsx @@ -13,7 +13,7 @@ import { type GridLayout as GridLayoutModel } from "../state/CallViewModel"; import styles from "./GridLayout.module.css"; import { useInitial } from "../useInitial"; import { type CallLayout, arrangeTiles } from "./CallLayout"; -import { type DragCallback, useUpdateLayout } from "./Grid"; +import { type DragCallback, useUpdateLayout, useVisibleTiles } from "./Grid"; interface GridCSSProperties extends CSSProperties { "--gap": string; @@ -73,6 +73,7 @@ export const makeGridLayout: CallLayout = ({ // The scrolling part of the layout is where all the grid tiles live scrolling: forwardRef(function GridLayout({ model, Slot }, ref) { useUpdateLayout(); + useVisibleTiles(model.setVisibleTiles); const { width, height: minHeight } = useObservableEagerState(minBounds); const { gap, tileWidth, tileHeight } = useMemo( () => arrangeTiles(width, minHeight, model.grid.length), @@ -93,13 +94,7 @@ export const makeGridLayout: CallLayout = ({ } > {model.grid.map((m) => ( - + ))}
); diff --git a/src/grid/OneOnOneLayout.tsx b/src/grid/OneOnOneLayout.tsx index e841a686..fb0af714 100644 --- a/src/grid/OneOnOneLayout.tsx +++ b/src/grid/OneOnOneLayout.tsx @@ -52,7 +52,6 @@ export const makeOneOnOneLayout: CallLayout = ({ @@ -61,7 +60,6 @@ export const makeOneOnOneLayout: CallLayout = ({ id={model.local.id} model={model.local} onDrag={onDragLocalTile} - onVisibilityChange={model.local.setVisible} data-block-alignment={pipAlignmentValue.block} data-inline-alignment={pipAlignmentValue.inline} /> diff --git a/src/grid/SpotlightExpandedLayout.tsx b/src/grid/SpotlightExpandedLayout.tsx index 371930ce..d4ce9af3 100644 --- a/src/grid/SpotlightExpandedLayout.tsx +++ b/src/grid/SpotlightExpandedLayout.tsx @@ -63,7 +63,6 @@ export const makeSpotlightExpandedLayout: CallLayout< id={model.pip.id} model={model.pip} onDrag={onDragPip} - onVisibilityChange={model.pip.setVisible} data-block-alignment={pipAlignmentValue.block} data-inline-alignment={pipAlignmentValue.inline} /> diff --git a/src/grid/SpotlightLandscapeLayout.tsx b/src/grid/SpotlightLandscapeLayout.tsx index 8596aec8..3b80a166 100644 --- a/src/grid/SpotlightLandscapeLayout.tsx +++ b/src/grid/SpotlightLandscapeLayout.tsx @@ -12,7 +12,7 @@ import classNames from "classnames"; import { type CallLayout } from "./CallLayout"; import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel"; import styles from "./SpotlightLandscapeLayout.module.css"; -import { useUpdateLayout } from "./Grid"; +import { useUpdateLayout, useVisibleTiles } from "./Grid"; /** * An implementation of the "spotlight landscape" layout, in which the spotlight @@ -50,6 +50,7 @@ export const makeSpotlightLandscapeLayout: CallLayout< ref, ) { useUpdateLayout(); + useVisibleTiles(model.setVisibleTiles); useObservableEagerState(minBounds); const withIndicators = useObservableEagerState(model.spotlight.media).length > 1; @@ -63,13 +64,7 @@ export const makeSpotlightLandscapeLayout: CallLayout< />
{model.grid.map((m) => ( - + ))}
diff --git a/src/grid/SpotlightPortraitLayout.tsx b/src/grid/SpotlightPortraitLayout.tsx index beeca3e6..6af0fa39 100644 --- a/src/grid/SpotlightPortraitLayout.tsx +++ b/src/grid/SpotlightPortraitLayout.tsx @@ -12,7 +12,7 @@ import classNames from "classnames"; import { type CallLayout, arrangeTiles } from "./CallLayout"; import { type SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel"; import styles from "./SpotlightPortraitLayout.module.css"; -import { useUpdateLayout } from "./Grid"; +import { useUpdateLayout, useVisibleTiles } from "./Grid"; interface GridCSSProperties extends CSSProperties { "--grid-gap": string; @@ -54,6 +54,7 @@ export const makeSpotlightPortraitLayout: CallLayout< ref, ) { useUpdateLayout(); + useVisibleTiles(model.setVisibleTiles); const { width } = useObservableEagerState(minBounds); const { gap, tileWidth, tileHeight } = arrangeTiles( width, @@ -84,13 +85,7 @@ export const makeSpotlightPortraitLayout: CallLayout< />
{model.grid.map((m) => ( - + ))}
diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel.test.ts index ad7e8702..dca21de2 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel.test.ts @@ -402,10 +402,7 @@ test("participants stay in the same order unless to appear/disappear", () => { // We imagine that only three tiles (the first three) will be visible // on screen at a time vm.layout.subscribe((layout) => { - if (layout.type === "grid") { - for (let i = 0; i < layout.grid.length; i++) - layout.grid[i].setVisible(i < 3); - } + if (layout.type === "grid") layout.setVisibleTiles(3); }); }, }); diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 72712aa9..108a44c9 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -143,18 +143,21 @@ export interface GridLayout { type: "grid"; spotlight?: SpotlightTileViewModel; grid: GridTileViewModel[]; + setVisibleTiles: (value: number) => void; } export interface SpotlightLandscapeLayout { type: "spotlight-landscape"; spotlight: SpotlightTileViewModel; grid: GridTileViewModel[]; + setVisibleTiles: (value: number) => void; } export interface SpotlightPortraitLayout { type: "spotlight-portrait"; spotlight: SpotlightTileViewModel; grid: GridTileViewModel[]; + setVisibleTiles: (value: number) => void; } export interface SpotlightExpandedLayout { @@ -223,7 +226,6 @@ enum SortingBin { interface LayoutScanState { layout: Layout | null; tiles: TileStore; - visibleTiles: Set; } class UserMedia { @@ -891,62 +893,53 @@ export class CallViewModel extends ViewModel { this.scope.state(), ); + // There is a cyclical dependency here: the layout algorithms want to know + // which tiles are on screen, but to know which tiles are on screen we have to + // first render a layout. To deal with this we assume initially that no tiles + // are visible, and loop the data back into the layouts with a Subject. + private readonly visibleTiles = new Subject(); + private readonly setVisibleTiles = (value: number): void => + this.visibleTiles.next(value); + public readonly layoutInternals: Observable< LayoutScanState & { layout: Layout } - > = this.layoutMedia.pipe( - // Each layout will produce a set of tiles, and these tiles have an - // observable indicating whether they're visible. We loop this information - // back into the layout process by using switchScan. - switchScan< - LayoutMedia, - LayoutScanState, - Observable + > = combineLatest([ + this.layoutMedia, + this.visibleTiles.pipe(startWith(0), distinctUntilChanged()), + ]).pipe( + scan< + [LayoutMedia, number], + LayoutScanState & { layout: Layout }, + LayoutScanState >( - ({ tiles: prevTiles, visibleTiles }, media) => { + ({ tiles: prevTiles }, [media, visibleTiles]) => { let layout: Layout; let newTiles: TileStore; switch (media.type) { case "grid": case "spotlight-landscape": case "spotlight-portrait": - [layout, newTiles] = gridLikeLayout(media, visibleTiles, prevTiles); - break; - case "spotlight-expanded": - [layout, newTiles] = spotlightExpandedLayout( + [layout, newTiles] = gridLikeLayout( media, visibleTiles, + this.setVisibleTiles, prevTiles, ); break; + case "spotlight-expanded": + [layout, newTiles] = spotlightExpandedLayout(media, prevTiles); + break; case "one-on-one": - [layout, newTiles] = oneOnOneLayout(media, visibleTiles, prevTiles); + [layout, newTiles] = oneOnOneLayout(media, prevTiles); break; case "pip": - [layout, newTiles] = pipLayout(media, visibleTiles, prevTiles); + [layout, newTiles] = pipLayout(media, prevTiles); break; } - // Take all of the 'visible' observables and combine them into one big - // observable array - const visibilities = - newTiles.gridTiles.length === 0 - ? of([]) - : combineLatest(newTiles.gridTiles.map((tile) => tile.visible)); - return visibilities.pipe( - map((visibilities) => ({ - layout: layout, - tiles: newTiles, - visibleTiles: new Set( - newTiles.gridTiles.filter((_tile, i) => visibilities[i]), - ), - })), - ); - }, - { - layout: null, - tiles: TileStore.empty(), - visibleTiles: new Set(), + return { layout, tiles: newTiles }; }, + { layout: null, tiles: TileStore.empty() }, ), this.scope.state(), ); diff --git a/src/state/GridLikeLayout.ts b/src/state/GridLikeLayout.ts index b846939e..e5a31cf6 100644 --- a/src/state/GridLikeLayout.ts +++ b/src/state/GridLikeLayout.ts @@ -7,7 +7,6 @@ Please see LICENSE in the repository root for full details. import { type Layout, type LayoutMedia } from "./CallViewModel"; import { type TileStore } from "./TileStore"; -import { type GridTileViewModel } from "./TileViewModel"; export type GridLikeLayoutType = | "grid" @@ -20,7 +19,8 @@ export type GridLikeLayoutType = */ export function gridLikeLayout( media: LayoutMedia & { type: GridLikeLayoutType }, - visibleTiles: Set, + visibleTiles: number, + setVisibleTiles: (value: number) => void, prevTiles: TileStore, ): [Layout & { type: GridLikeLayoutType }, TileStore] { const update = prevTiles.from(visibleTiles); @@ -37,6 +37,7 @@ export function gridLikeLayout( type: media.type, spotlight: tiles.spotlightTile, grid: tiles.gridTiles, + setVisibleTiles, } as Layout & { type: GridLikeLayoutType }, tiles, ]; diff --git a/src/state/OneOnOneLayout.ts b/src/state/OneOnOneLayout.ts index f078b7e3..2a0e7ff5 100644 --- a/src/state/OneOnOneLayout.ts +++ b/src/state/OneOnOneLayout.ts @@ -7,17 +7,15 @@ Please see LICENSE in the repository root for full details. import { type OneOnOneLayout, type OneOnOneLayoutMedia } from "./CallViewModel"; import { type TileStore } from "./TileStore"; -import { type GridTileViewModel } from "./TileViewModel"; /** * Produces a one-on-one layout with the given media. */ export function oneOnOneLayout( media: OneOnOneLayoutMedia, - visibleTiles: Set, prevTiles: TileStore, ): [OneOnOneLayout, TileStore] { - const update = prevTiles.from(visibleTiles); + const update = prevTiles.from(2); update.registerGridTile(media.local); update.registerGridTile(media.remote); const tiles = update.build(); diff --git a/src/state/PipLayout.ts b/src/state/PipLayout.ts index 79187752..ad56cdd5 100644 --- a/src/state/PipLayout.ts +++ b/src/state/PipLayout.ts @@ -7,17 +7,15 @@ Please see LICENSE in the repository root for full details. import { type PipLayout, type PipLayoutMedia } from "./CallViewModel"; import { type TileStore } from "./TileStore"; -import { type GridTileViewModel } from "./TileViewModel"; /** * Produces a picture-in-picture layout with the given media. */ export function pipLayout( media: PipLayoutMedia, - visibleTiles: Set, prevTiles: TileStore, ): [PipLayout, TileStore] { - const update = prevTiles.from(visibleTiles); + const update = prevTiles.from(0); update.registerSpotlight(media.spotlight, true); const tiles = update.build(); return [ diff --git a/src/state/SpotlightExpandedLayout.ts b/src/state/SpotlightExpandedLayout.ts index 56b1d01d..c14b24a7 100644 --- a/src/state/SpotlightExpandedLayout.ts +++ b/src/state/SpotlightExpandedLayout.ts @@ -10,17 +10,15 @@ import { type SpotlightExpandedLayoutMedia, } from "./CallViewModel"; import { type TileStore } from "./TileStore"; -import { type GridTileViewModel } from "./TileViewModel"; /** * Produces an expanded spotlight layout with the given media. */ export function spotlightExpandedLayout( media: SpotlightExpandedLayoutMedia, - visibleTiles: Set, prevTiles: TileStore, ): [SpotlightExpandedLayout, TileStore] { - const update = prevTiles.from(visibleTiles); + const update = prevTiles.from(1); update.registerSpotlight(media.spotlight, true); if (media.pip !== undefined) update.registerGridTile(media.pip); const tiles = update.build(); diff --git a/src/state/TileStore.ts b/src/state/TileStore.ts index 2464d9eb..cd269944 100644 --- a/src/state/TileStore.ts +++ b/src/state/TileStore.ts @@ -101,7 +101,7 @@ export class TileStore { * Creates a builder which can be used to update the collection, passing * ownership of the tiles to the updated collection. */ - public from(visibleTiles: Set): TileStoreBuilder { + public from(visibleTiles: number): TileStoreBuilder { return new TileStoreBuilder( this.spotlight, this.grid, @@ -146,7 +146,7 @@ export class TileStoreBuilder { spotlight: SpotlightTileData | null, grid: GridTileData[], ) => TileStore, - private readonly visibleTiles: Set, + private readonly visibleTiles: number, /** * A number incremented on each update, just for debugging purposes. */ @@ -204,10 +204,8 @@ export class TileStoreBuilder { const prev = this.prevGridByMedia.get(this.spotlight.media[0]); if (prev !== undefined) { const [entry, prevIndex] = prev; - const previouslyVisible = this.visibleTiles.has(entry.vm); - const nowVisible = this.visibleTiles.has( - this.prevGrid[this.numGridEntries]?.vm, - ); + const previouslyVisible = prevIndex < this.visibleTiles; + const nowVisible = this.numGridEntries < this.visibleTiles; // If it doesn't need to move between the visible/invisible sections of // the grid, then we can keep it where it was and swap the media @@ -236,17 +234,15 @@ export class TileStoreBuilder { const prev = this.prevGridByMedia.get(media); if (prev === undefined) { // Create a new tile - (this.visibleTiles.has(this.prevGrid[this.numGridEntries]?.vm) + (this.numGridEntries < this.visibleTiles ? this.visibleGridEntries : this.invisibleGridEntries ).push(new GridTileData(media)); } else { // Reuse the existing tile const [entry, prevIndex] = prev; - const previouslyVisible = this.visibleTiles.has(entry.vm); - const nowVisible = this.visibleTiles.has( - this.prevGrid[this.numGridEntries]?.vm, - ); + const previouslyVisible = prevIndex < this.visibleTiles; + const nowVisible = this.numGridEntries < this.visibleTiles; // If it doesn't need to move between the visible/invisible sections of // the grid, then we can keep it exactly where it was previously if (previouslyVisible === nowVisible) diff --git a/src/state/TileViewModel.ts b/src/state/TileViewModel.ts index 53bc8648..612d7033 100644 --- a/src/state/TileViewModel.ts +++ b/src/state/TileViewModel.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { BehaviorSubject, type Observable } from "rxjs"; +import { type Observable } from "rxjs"; import { ViewModel } from "./ViewModel"; import { type MediaViewModel, type UserMediaViewModel } from "./MediaViewModel"; @@ -18,14 +18,6 @@ function createId(): string { export class GridTileViewModel extends ViewModel { public readonly id = createId(); - private readonly visible_ = new BehaviorSubject(false); - /** - * Whether the tile is visible within the current viewport. - */ - public readonly visible: Observable = this.visible_; - - public setVisible = (value: boolean): void => this.visible_.next(value); - public constructor(public readonly media: Observable) { super(); } From 53565ddb7677222132ac972e3c5f299a58d05be8 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 12 Dec 2024 19:16:01 -0500 Subject: [PATCH 04/19] Test that participants adjust order when screen size changes --- src/state/CallViewModel.test.ts | 62 +++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel.test.ts index dca21de2..0e45faa1 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel.test.ts @@ -376,16 +376,16 @@ test("screen sharing activates spotlight layout", () => { test("participants stay in the same order unless to appear/disappear", () => { withTestScheduler(({ hot, schedule, expectObservable }) => { - const modeInputMarbles = " a"; + const visibilityInputMarbles = "a"; // First Bob speaks, then Dave, then Alice - const aSpeakingInputMarbles = "n- 1998ms - 1999ms y"; - const bSpeakingInputMarbles = "ny 1998ms n 1999ms -"; - const dSpeakingInputMarbles = "n- 1998ms y 1999ms n"; + const aSpeakingInputMarbles = " n- 1998ms - 1999ms y"; + const bSpeakingInputMarbles = " ny 1998ms n 1999ms -"; + const dSpeakingInputMarbles = " n- 1998ms y 1999ms n"; // Nothing should change when Bob speaks, because Bob is already on screen. // When Dave speaks he should switch with Alice because she's the one who // hasn't spoken at all. Then when Alice speaks, she should return to her // place at the top. - const expectedLayoutMarbles = "a 1999ms b 1999ms a 57999ms c 1999ms a"; + const expectedLayoutMarbles = " a 1999ms b 1999ms a 57999ms c 1999ms a"; withCallViewModel( of([aliceParticipant, bobParticipant, daveParticipant]), @@ -397,7 +397,7 @@ test("participants stay in the same order unless to appear/disappear", () => { [daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })], ]), (vm) => { - schedule(modeInputMarbles, { + schedule(visibilityInputMarbles, { a: () => { // We imagine that only three tiles (the first three) will be visible // on screen at a time @@ -432,6 +432,56 @@ test("participants stay in the same order unless to appear/disappear", () => { }); }); +test("participants adjust order when space becomes constrained", () => { + withTestScheduler(({ hot, schedule, expectObservable }) => { + // Start with all tiles on screen then shrink to 3 + const visibilityInputMarbles = "a-b"; + // Bob and Dave speak + const bSpeakingInputMarbles = " ny"; + const dSpeakingInputMarbles = " ny"; + // Nothing should change when Bob or Dave initially speak, because they are + // on screen. When the screen becomes smaller Alice should move off screen + // to make way for the speakers (specifically, she should swap with Dave). + const expectedLayoutMarbles = " a-b"; + + withCallViewModel( + of([aliceParticipant, bobParticipant, daveParticipant]), + of([aliceRtcMember, bobRtcMember, daveRtcMember]), + of(ConnectionState.Connected), + new Map([ + [bobParticipant, hot(bSpeakingInputMarbles, { y: true, n: false })], + [daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })], + ]), + (vm) => { + let setVisibleTiles: ((value: number) => void) | null = null; + vm.layout.subscribe((layout) => { + if (layout.type === "grid") setVisibleTiles = layout.setVisibleTiles; + }); + schedule(visibilityInputMarbles, { + a: () => setVisibleTiles!(Infinity), + b: () => setVisibleTiles!(3), + }); + + expectObservable(summarizeLayout(vm.layout)).toBe( + expectedLayoutMarbles, + { + a: { + type: "grid", + spotlight: undefined, + grid: ["local:0", `${aliceId}:0`, `${bobId}:0`, `${daveId}:0`], + }, + b: { + type: "grid", + spotlight: undefined, + grid: ["local:0", `${daveId}:0`, `${bobId}:0`, `${aliceId}:0`], + }, + }, + ); + }, + ); + }); +}); + test("spotlight speakers swap places", () => { withTestScheduler(({ hot, schedule, expectObservable }) => { // Go immediately into spotlight mode for the test From 7807c44fdcb7df8ed688ccd39a09da2cb0273eee Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 13 Dec 2024 10:09:27 +0000 Subject: [PATCH 05/19] Clean up useMuteStates test (#2891) We don't need to be mocking React for this test. --- src/room/MuteStates.test.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx index 77ab8ace..0db95a53 100644 --- a/src/room/MuteStates.test.tsx +++ b/src/room/MuteStates.test.tsx @@ -6,8 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; -import React, { type ReactNode } from "react"; -import { beforeEach } from "vitest"; +import { type ReactNode } from "react"; import { render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; @@ -92,16 +91,12 @@ function mockMediaDevices( } describe("useMuteStates", () => { - beforeEach(() => { - vi.spyOn(React, "useContext").mockReturnValue({}); - }); - afterEach(() => { - vi.restoreAllMocks(); + vi.clearAllMocks(); }); afterAll(() => { - vi.clearAllMocks(); + vi.resetAllMocks(); }); it("disabled when no input devices", () => { From e84ecc68b3ee4e3dd9d090f8f4c96051aa1c095d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 13 Dec 2024 13:23:32 +0000 Subject: [PATCH 06/19] Refactor preferences tab strings for consistency (#2888) * Refactor preferences tab strings for consistency * Lint --- locales/de/app.json | 8 ++++---- locales/en/app.json | 8 ++++---- locales/ro/app.json | 8 ++++---- src/settings/PreferencesSettingsTab.tsx | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/locales/de/app.json b/locales/de/app.json index 67138ca5..3ba4a36a 100644 --- a/locales/de/app.json +++ b/locales/de/app.json @@ -161,16 +161,16 @@ "more_tab_title": "Mehr", "opt_in_description": "<0><1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.", "preferences_tab": { + "introduction": "Hier können zusätzliche Optionen für individuelle Anforderungen eingestellt werden", "reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet", "reactions_play_sound_label": "Reaktionstöne abspielen", "reactions_show_description": "Zeige eine Animation, wenn jemand eine Reaktion sendet.", "reactions_show_label": "Reaktionen anzeigen", - "reactions_title": "Reaktionen" + "reactions_title": "Reaktionen", + "show_hand_raised_timer_description": "Einen Timer zur Handmeldung anzeigen", + "show_hand_raised_timer_label": "Dauer der Handmeldung anzeigen" }, - "preferences_tab_body": "Hier können zusätzliche Optionen für individuelle Anforderungen eingestellt werden", "preferences_tab_h4": "Einstellungen", - "preferences_tab_show_hand_raised_timer_description": "Einen Timer zur Handmeldung anzeigen", - "preferences_tab_show_hand_raised_timer_label": "Dauer der Handmeldung anzeigen", "speaker_device_selection_label": "Lautsprecher" }, "star_rating_input_label_one": "{{count}} Stern", diff --git a/locales/en/app.json b/locales/en/app.json index 0d2b5f15..84038c2b 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -162,14 +162,14 @@ "preferences_tab": { "developer_mode_label": "Developer mode", "developer_mode_label_description": "Enable developer mode and show developer settings tab.", + "introduction": "Here you can configure extra options for an improved experience.", "reactions_play_sound_description": "Play a sound effect when anyone sends a reaction into a call.", "reactions_play_sound_label": "Play reaction sounds", "reactions_show_description": "Show an animation when anyone sends a reaction.", - "reactions_show_label": "Show reactions" + "reactions_show_label": "Show reactions", + "show_hand_raised_timer_description": "Show a timer when a participant raises their hand", + "show_hand_raised_timer_label": "Show hand raise duration" }, - "preferences_tab_body": "Here you can configure extra options for an improved experience.", - "preferences_tab_show_hand_raised_timer_description": "Show a timer when a participant raises their hand", - "preferences_tab_show_hand_raised_timer_label": "Show hand raise duration", "speaker_device_selection_label": "Speaker" }, "star_rating_input_label_one": "{{count}} star", diff --git a/locales/ro/app.json b/locales/ro/app.json index ab55c511..6d0de16f 100644 --- a/locales/ro/app.json +++ b/locales/ro/app.json @@ -160,16 +160,16 @@ "more_tab_title": "Mai mult", "opt_in_description": "<0><1>Puteți retrage consimțământul debifând această casetă. Dacă sunteți în prezent la un apel, această setare va intra în vigoare la sfârșitul apelului.", "preferences_tab": { + "introduction": "Aici puteți configura opțiuni suplimentare pentru o experiență îmbunătățită", "reactions_play_sound_description": "Redați un efect sonor atunci când cineva trimite o reacție la un apel.", "reactions_play_sound_label": "Redați sunete de reacție", "reactions_show_description": "Afișați o animație atunci când cineva trimite o reacție.", "reactions_show_label": "Afișați reacțiile", - "reactions_title": "Reacții" + "reactions_title": "Reacții", + "show_hand_raised_timer_description": "Afișați un cronometru atunci când un participant ridică mâna", + "show_hand_raised_timer_label": "Afișați durata ridicării mâinii" }, - "preferences_tab_body": "Aici puteți configura opțiuni suplimentare pentru o experiență îmbunătățită", "preferences_tab_h4": "preferinte", - "preferences_tab_show_hand_raised_timer_description": "Afișați un cronometru atunci când un participant ridică mâna", - "preferences_tab_show_hand_raised_timer_label": "Afișați durata ridicării mâinii", "speaker_device_selection_label": "vorbitor" }, "start_new_call": "Începe un nou apel", diff --git a/src/settings/PreferencesSettingsTab.tsx b/src/settings/PreferencesSettingsTab.tsx index fd4be1ae..72d2d919 100644 --- a/src/settings/PreferencesSettingsTab.tsx +++ b/src/settings/PreferencesSettingsTab.tsx @@ -41,13 +41,13 @@ export const PreferencesSettingsTab: FC = () => { return (
- {t("settings.preferences_tab_body")} + {t("settings.preferences_tab.introduction")} Date: Fri, 13 Dec 2024 14:12:29 +0000 Subject: [PATCH 07/19] Bump matrix-js-sdk (#2897) So that we get https://github.com/matrix-org/matrix-js-sdk/pull/4575 Full diff: https://github.com/matrix-org/matrix-js-sdk/compare/edac6a9983bd604c17535a9ae673dc979c7b61c4...d1de32ea2773df4c6f8a956678bbd19b6d022475 --- yarn.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 75c2ef72..d607f394 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1961,10 +1961,10 @@ dependencies: "@bufbuild/protobuf" "^1.10.0" -"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz#f889653eb4fafaad2a963654d586bd34de62acd5" - integrity sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og== +"@matrix-org/matrix-sdk-crypto-wasm@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.0.0.tgz#c49a1a0d1e367d3c00a2144a4ab23caee0b1eec2" + integrity sha512-a7NUH8Kjc8hwzNCPpkOGXoceFqWJiWvA8OskXeDrKyODJuDz4yKrZ/nvgaVRfQe45Ab5UC1ZXYqaME+ChlJuqg== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -6494,11 +6494,11 @@ matrix-events-sdk@0.0.1: integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== matrix-js-sdk@matrix-org/matrix-js-sdk#develop: - version "34.12.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/edac6a9983bd604c17535a9ae673dc979c7b61c4" + version "34.13.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d1de32ea2773df4c6f8a956678bbd19b6d022475" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^11.0.0" "@matrix-org/olm" "3.2.15" another-json "^0.2.0" bs58 "^6.0.0" From 31be6cc804c0a9654caaafed73d5d6a3757fa736 Mon Sep 17 00:00:00 2001 From: robintown <48614497+robintown@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:28:28 +0000 Subject: [PATCH 08/19] Translations updates --- locales/bg/app.json | 1 - locales/cs/app.json | 3 --- locales/de/app.json | 11 ++--------- locales/el/app.json | 3 --- locales/es/app.json | 3 --- locales/et/app.json | 3 --- locales/fa/app.json | 1 - locales/fr/app.json | 3 --- locales/id/app.json | 3 --- locales/it/app.json | 3 --- locales/lv/app.json | 3 --- locales/pl/app.json | 3 --- locales/ro/app.json | 10 +--------- locales/ru/app.json | 3 --- locales/sk/app.json | 3 --- locales/tr/app.json | 3 +-- locales/uk/app.json | 3 --- locales/vi/app.json | 1 - locales/zh-Hans/app.json | 3 --- locales/zh-Hant/app.json | 3 --- 20 files changed, 4 insertions(+), 65 deletions(-) diff --git a/locales/bg/app.json b/locales/bg/app.json index 4d51f573..eb9e5704 100644 --- a/locales/bg/app.json +++ b/locales/bg/app.json @@ -62,7 +62,6 @@ "developer_tab_title": "Разработчик", "feedback_tab_h4": "Изпрати обратна връзка", "feedback_tab_send_logs_label": "Включи debug логове", - "more_tab_title": "Още", "speaker_device_selection_label": "Говорител" }, "unauthenticated_view_body": "Все още не сте регистрирани? <2>Създайте акаунт", diff --git a/locales/cs/app.json b/locales/cs/app.json index 8089af9a..b9793e1f 100644 --- a/locales/cs/app.json +++ b/locales/cs/app.json @@ -60,12 +60,9 @@ "return_home_button": "Vrátit se na domácí obrazovku", "screenshare_button_label": "Sdílet obrazovku", "settings": { - "developer_settings_label": "Vývojářské nastavení", - "developer_settings_label_description": "Zobrazit vývojářské nastavení.", "developer_tab_title": "Vývojář", "feedback_tab_h4": "Dát feedback", "feedback_tab_send_logs_label": "Zahrnout ladící záznamy", - "more_tab_title": "Více", "speaker_device_selection_label": "Reproduktor" }, "unauthenticated_view_body": "Nejste registrovaní? <2>Vytvořit účet", diff --git a/locales/de/app.json b/locales/de/app.json index 3ba4a36a..2c35b341 100644 --- a/locales/de/app.json +++ b/locales/de/app.json @@ -147,10 +147,9 @@ "screenshare_button_label": "Bildschirm teilen", "settings": { "audio_tab": { + "effect_volume_description": "Lautstärke anpassen, mit der Reaktionen und Handmeldungen abgespielt werden", "effect_volume_label": "Lautstärke der Soundeffekte" }, - "developer_settings_label": "Entwicklereinstellungen", - "developer_settings_label_description": "Zeige die Entwicklereinstellungen im Einstellungsfenster.", "developer_tab_title": "Entwickler", "feedback_tab_body": "Falls du auf Probleme stößt oder einfach nur eine Rückmeldung geben möchtest, sende uns bitte eine kurze Beschreibung.", "feedback_tab_description_label": "Deine Rückmeldung", @@ -158,19 +157,13 @@ "feedback_tab_send_logs_label": "Debug-Protokolle anhängen", "feedback_tab_thank_you": "Danke, wir haben deine Rückmeldung erhalten!", "feedback_tab_title": "Rückmeldung", - "more_tab_title": "Mehr", "opt_in_description": "<0><1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.", "preferences_tab": { - "introduction": "Hier können zusätzliche Optionen für individuelle Anforderungen eingestellt werden", "reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet", "reactions_play_sound_label": "Reaktionstöne abspielen", "reactions_show_description": "Zeige eine Animation, wenn jemand eine Reaktion sendet.", - "reactions_show_label": "Reaktionen anzeigen", - "reactions_title": "Reaktionen", - "show_hand_raised_timer_description": "Einen Timer zur Handmeldung anzeigen", - "show_hand_raised_timer_label": "Dauer der Handmeldung anzeigen" + "reactions_show_label": "Reaktionen anzeigen" }, - "preferences_tab_h4": "Einstellungen", "speaker_device_selection_label": "Lautsprecher" }, "star_rating_input_label_one": "{{count}} Stern", diff --git a/locales/el/app.json b/locales/el/app.json index c4b7a7a9..10b9396d 100644 --- a/locales/el/app.json +++ b/locales/el/app.json @@ -67,8 +67,6 @@ "return_home_button": "Επιστροφή στην αρχική οθόνη", "screenshare_button_label": "Κοινή χρήση οθόνης", "settings": { - "developer_settings_label": "Ρυθμίσεις προγραμματιστή", - "developer_settings_label_description": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.", "developer_tab_title": "Προγραμματιστής", "feedback_tab_body": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.", "feedback_tab_description_label": "Τα σχόλιά σας", @@ -76,7 +74,6 @@ "feedback_tab_send_logs_label": "Να συμπεριληφθούν αρχεία καταγραφής", "feedback_tab_thank_you": "Ευχαριστούμε, λάβαμε τα σχόλιά σας!", "feedback_tab_title": "Ανατροφοδότηση", - "more_tab_title": "Περισσότερα", "opt_in_description": "<0><1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.", "speaker_device_selection_label": "Ηχείο" }, diff --git a/locales/es/app.json b/locales/es/app.json index c9e9ee1a..96f6710c 100644 --- a/locales/es/app.json +++ b/locales/es/app.json @@ -67,8 +67,6 @@ "room_auth_view_eula_caption": "Al hacer clic en \"Unirse a la llamada ahora\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)", "screenshare_button_label": "Compartir pantalla", "settings": { - "developer_settings_label": "Ajustes de desarrollador", - "developer_settings_label_description": "Muestra los ajustes de desarrollador en la ventana de ajustes.", "developer_tab_title": "Desarrollador", "feedback_tab_body": "Si tienes algún problema o simplemente quieres darnos tu opinión, por favor envíanos una breve descripción.", "feedback_tab_description_label": "Tus comentarios", @@ -76,7 +74,6 @@ "feedback_tab_send_logs_label": "Incluir registros de depuración", "feedback_tab_thank_you": "¡Gracias, hemos recibido tus comentarios!", "feedback_tab_title": "Danos tu opinión", - "more_tab_title": "Más", "opt_in_description": "<0><1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta.", "speaker_device_selection_label": "Altavoz" }, diff --git a/locales/et/app.json b/locales/et/app.json index bdce05f6..ccd1c699 100644 --- a/locales/et/app.json +++ b/locales/et/app.json @@ -97,8 +97,6 @@ "room_auth_view_eula_caption": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)", "screenshare_button_label": "Jaga ekraani", "settings": { - "developer_settings_label": "Arendaja seadistused", - "developer_settings_label_description": "Näita seadistuste aknas arendajale vajalikke seadeid.", "developer_tab_title": "Arendaja", "feedback_tab_body": "Kui selle rakenduse kasutamisel tekib sul probleeme või lihtsalt soovid oma arvamust avaldada, siis palun täida alljärgnev lühike kirjeldus.", "feedback_tab_description_label": "Sinu tagasiside", @@ -106,7 +104,6 @@ "feedback_tab_send_logs_label": "Lisa veatuvastuslogid", "feedback_tab_thank_you": "Tänud, me oleme sinu tagasiside kätte saanud!", "feedback_tab_title": "Tagasiside", - "more_tab_title": "Rohkem", "opt_in_description": "<0><1>Sa võid selle valiku eelmaldamisega alati oma nõusoleku tagasi võtta. Kui sul parasjagu on kõne pooleli, siis seadistuste muudatus jõustub pärast kõne lõppu.", "speaker_device_selection_label": "Kõlar" }, diff --git a/locales/fa/app.json b/locales/fa/app.json index 7fdf98a2..125ec785 100644 --- a/locales/fa/app.json +++ b/locales/fa/app.json @@ -64,7 +64,6 @@ "developer_tab_title": "توسعه دهنده", "feedback_tab_h4": "بازخورد ارائه دهید", "feedback_tab_send_logs_label": "شامل لاگ‌های عیب‌یابی", - "more_tab_title": "بیشتر", "speaker_device_selection_label": "بلندگو" }, "unauthenticated_view_body": "هنوز ثبت‌نام نکرده‌اید؟ <2>ساخت حساب کاربری", diff --git a/locales/fr/app.json b/locales/fr/app.json index 4cb2b9e4..f465244d 100644 --- a/locales/fr/app.json +++ b/locales/fr/app.json @@ -95,8 +95,6 @@ "room_auth_view_eula_caption": "En cliquant sur « Rejoindre l’appel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)", "screenshare_button_label": "Partage d’écran", "settings": { - "developer_settings_label": "Paramètres développeurs", - "developer_settings_label_description": "Affiche les paramètres développeurs dans la fenêtre des paramètres.", "developer_tab_title": "Développeur", "feedback_tab_body": "Si vous rencontrez des problèmes, ou vous voulez simplement faire un commentaire, faites-en une courte description ci-dessous.", "feedback_tab_description_label": "Votre commentaire", @@ -104,7 +102,6 @@ "feedback_tab_send_logs_label": "Inclure les journaux de débogage", "feedback_tab_thank_you": "Merci, nous avons reçu vos commentaires !", "feedback_tab_title": "Commentaires", - "more_tab_title": "Plus", "opt_in_description": "<0><1>Vous pouvez retirer votre consentement en décochant cette case. Si vous êtes actuellement en communication, ce paramètre prendra effet à la fin de l’appel.", "speaker_device_selection_label": "Intervenant" }, diff --git a/locales/id/app.json b/locales/id/app.json index c1c7fae3..c479d604 100644 --- a/locales/id/app.json +++ b/locales/id/app.json @@ -96,8 +96,6 @@ "room_auth_view_eula_caption": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA) kami", "screenshare_button_label": "Bagikan layar", "settings": { - "developer_settings_label": "Pengaturan Pengembang", - "developer_settings_label_description": "Ekspos pengaturan pengembang dalam jendela pengaturan.", "developer_tab_title": "Pengembang", "feedback_tab_body": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.", "feedback_tab_description_label": "Masukan Anda", @@ -105,7 +103,6 @@ "feedback_tab_send_logs_label": "Termasuk catatan pengawakutuan", "feedback_tab_thank_you": "Terima kasih, kami telah menerima masukan Anda!", "feedback_tab_title": "Masukan", - "more_tab_title": "Lainnya", "opt_in_description": "<0><1>Anda dapat mengurungkan kembali izin dengan mencentang kotak ini. Jika Anda saat ini dalam panggilan, pengaturan ini akan diterapkan di akhir panggilan.", "speaker_device_selection_label": "Pembicara" }, diff --git a/locales/it/app.json b/locales/it/app.json index a3d1d797..6fe08427 100644 --- a/locales/it/app.json +++ b/locales/it/app.json @@ -94,15 +94,12 @@ "room_auth_view_eula_caption": "Cliccando \"Entra in chiamata ora\", accetti il nostro <2>accordo di licenza con l'utente finale (EULA)", "screenshare_button_label": "Condividi schermo", "settings": { - "developer_settings_label": "Impostazioni per sviluppatori", - "developer_settings_label_description": "Mostra le impostazioni per sviluppatori nella finestra delle impostazioni.", "developer_tab_title": "Sviluppatore", "feedback_tab_body": "Se stai riscontrando problemi o semplicemente vuoi dare un'opinione, inviaci una breve descrizione qua sotto.", "feedback_tab_description_label": "Il tuo commento", "feedback_tab_h4": "Invia commento", "feedback_tab_send_logs_label": "Includi registri di debug", "feedback_tab_thank_you": "Grazie, abbiamo ricevuto il tuo commento!", - "more_tab_title": "Altro", "opt_in_description": "<0><1>Puoi revocare il consenso deselezionando questa casella. Se attualmente sei in una chiamata, avrà effetto al termine di essa.", "speaker_device_selection_label": "Altoparlante" }, diff --git a/locales/lv/app.json b/locales/lv/app.json index a92d4bf2..ee48986f 100644 --- a/locales/lv/app.json +++ b/locales/lv/app.json @@ -75,8 +75,6 @@ "room_auth_view_eula_caption": "Klikšķināšana uz \"Pievienoties zvanam tagad\" apliecina piekrišanu mūsu <2>galalietotāja licencēšanas nolīgumam (GLLN)", "screenshare_button_label": "Kopīgot ekrānu", "settings": { - "developer_settings_label": "Izstrādātāja iestatījumi", - "developer_settings_label_description": "Izstādīt izstrādātāja iestatījumus iestatījumu logā.", "developer_tab_title": "Izstrādātājs", "feedback_tab_body": "Ja tiek piedzīvoti sarežģījumi vai vienkārši ir vēlme sniegt kādu atsauksmi, lūgums zemāk nosūtīt mums īsu aprakstu.", "feedback_tab_description_label": "Tava atsauksme", @@ -84,7 +82,6 @@ "feedback_tab_send_logs_label": "Iekļaut atkļūdošanas žurnāla ierakstus", "feedback_tab_thank_you": "Paldies, mēs saņēmām atsauksmi!", "feedback_tab_title": "Atsauksmes", - "more_tab_title": "Vairāk", "opt_in_description": "<0><1>Savu piekrišanu var atsaukt ar atzīmes noņemšanu no šīs rūtiņas. Ja pašreiz atrodies zvanā, šis iestatījums stāsies spēkā zvana beigās.", "speaker_device_selection_label": "Runātājs" }, diff --git a/locales/pl/app.json b/locales/pl/app.json index 7b27c488..db3986ef 100644 --- a/locales/pl/app.json +++ b/locales/pl/app.json @@ -99,8 +99,6 @@ "room_auth_view_eula_caption": "Klikając \"Dołącz teraz do rozmowy\", zgadzasz się na naszą <2>Umowę licencyjną (EULA)", "screenshare_button_label": "Udostępnij ekran", "settings": { - "developer_settings_label": "Opcje programisty", - "developer_settings_label_description": "Wyświetl opcje programisty w oknie ustawień.", "developer_tab_title": "Programista", "feedback_tab_body": "Jeśli posiadasz problemy lub chciałbyś zgłosić swoją opinię, wyślij nam krótki opis.", "feedback_tab_description_label": "Twoje opinie", @@ -108,7 +106,6 @@ "feedback_tab_send_logs_label": "Dołącz dzienniki debugowania", "feedback_tab_thank_you": "Dziękujemy, otrzymaliśmy Twoją opinię!", "feedback_tab_title": "Opinia użytkownika", - "more_tab_title": "Więcej", "opt_in_description": "<0><1>Możesz wycofać swoją zgodę poprzez odznaczenie tego pola. Jeśli już jesteś w trakcie rozmowy, opcja zostanie zastosowana po jej zakończeniu.", "speaker_device_selection_label": "Głośnik" }, diff --git a/locales/ro/app.json b/locales/ro/app.json index 6d0de16f..0b83b0f3 100644 --- a/locales/ro/app.json +++ b/locales/ro/app.json @@ -148,8 +148,6 @@ "effect_volume_description": "Reglați volumul la care reacționează reacțiile și efectele ridicate de mână", "effect_volume_label": "Volumul efectului sonor" }, - "developer_settings_label": "Setări pentru dezvoltatori", - "developer_settings_label_description": "Expuneți setările dezvoltatorului în fereastra de setări.", "developer_tab_title": "dezvoltator", "feedback_tab_body": "Dacă întâmpinați probleme sau pur și simplu doriți să oferiți feedback, vă rugăm să ne trimiteți o scurtă descriere mai jos.", "feedback_tab_description_label": "Feedback-ul tău", @@ -157,19 +155,13 @@ "feedback_tab_send_logs_label": "Includeți jurnale de depanare", "feedback_tab_thank_you": "Vă mulțumim, am primit feedback-ul dvs.!", "feedback_tab_title": "Feedback", - "more_tab_title": "Mai mult", "opt_in_description": "<0><1>Puteți retrage consimțământul debifând această casetă. Dacă sunteți în prezent la un apel, această setare va intra în vigoare la sfârșitul apelului.", "preferences_tab": { - "introduction": "Aici puteți configura opțiuni suplimentare pentru o experiență îmbunătățită", "reactions_play_sound_description": "Redați un efect sonor atunci când cineva trimite o reacție la un apel.", "reactions_play_sound_label": "Redați sunete de reacție", "reactions_show_description": "Afișați o animație atunci când cineva trimite o reacție.", - "reactions_show_label": "Afișați reacțiile", - "reactions_title": "Reacții", - "show_hand_raised_timer_description": "Afișați un cronometru atunci când un participant ridică mâna", - "show_hand_raised_timer_label": "Afișați durata ridicării mâinii" + "reactions_show_label": "Afișați reacțiile" }, - "preferences_tab_h4": "preferinte", "speaker_device_selection_label": "vorbitor" }, "start_new_call": "Începe un nou apel", diff --git a/locales/ru/app.json b/locales/ru/app.json index 0836b2bb..725fffdd 100644 --- a/locales/ru/app.json +++ b/locales/ru/app.json @@ -69,8 +69,6 @@ "return_home_button": "Вернуться в Начало", "screenshare_button_label": "Поделиться экраном", "settings": { - "developer_settings_label": "Настройки Разработчика", - "developer_settings_label_description": "Раскрыть настройки разработчика в окне настроек.", "developer_tab_title": "Разработчику", "feedback_tab_body": "Если у вас возникли проблемы или вы просто хотите оставить отзыв, отправьте нам краткое описание ниже.", "feedback_tab_description_label": "Ваш отзыв", @@ -78,7 +76,6 @@ "feedback_tab_send_logs_label": "Приложить журнал отладки", "feedback_tab_thank_you": "Спасибо. Мы получили ваш отзыв!", "feedback_tab_title": "Отзыв", - "more_tab_title": "Больше", "opt_in_description": "<0><1>Вы можете отозвать согласие, сняв этот флажок. Если вы в данный момент находитесь в разговоре, эта настройка вступит в силу по окончании разговора.", "speaker_device_selection_label": "Динамик" }, diff --git a/locales/sk/app.json b/locales/sk/app.json index 48d69430..fbf2dc37 100644 --- a/locales/sk/app.json +++ b/locales/sk/app.json @@ -97,8 +97,6 @@ "room_auth_view_eula_caption": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)", "screenshare_button_label": "Zdieľať obrazovku", "settings": { - "developer_settings_label": "Nastavenia pre vývojárov", - "developer_settings_label_description": "Zobraziť nastavenia pre vývojárov v okne nastavení.", "developer_tab_title": "Vývojár", "feedback_tab_body": "Ak máte problémy alebo jednoducho chcete poskytnúť spätnú väzbu, pošlite nám krátky popis nižšie.", "feedback_tab_description_label": "Vaša spätná väzba", @@ -106,7 +104,6 @@ "feedback_tab_send_logs_label": "Zahrnúť záznamy o ladení", "feedback_tab_thank_you": "Ďakujeme, dostali sme vašu spätnú väzbu!", "feedback_tab_title": "Spätná väzba", - "more_tab_title": "Viac", "opt_in_description": "<0><1>Súhlas môžete odvolať zrušením označenia tohto políčka. Ak práve prebieha hovor, toto nastavenie nadobudne platnosť po skončení hovoru.", "speaker_device_selection_label": "Reproduktor" }, diff --git a/locales/tr/app.json b/locales/tr/app.json index 8be1769e..d14f6883 100644 --- a/locales/tr/app.json +++ b/locales/tr/app.json @@ -52,8 +52,7 @@ "settings": { "developer_tab_title": "Geliştirici", "feedback_tab_h4": "Geri bildirim ver", - "feedback_tab_send_logs_label": "Hata ayıklama kütüğünü dahil et", - "more_tab_title": "Daha" + "feedback_tab_send_logs_label": "Hata ayıklama kütüğünü dahil et" }, "unauthenticated_view_body": "Kaydolmadınız mı? <2>Hesap açın", "unauthenticated_view_login_button": "Hesabınıza girin" diff --git a/locales/uk/app.json b/locales/uk/app.json index f88d14a8..4faa19df 100644 --- a/locales/uk/app.json +++ b/locales/uk/app.json @@ -99,8 +99,6 @@ "room_auth_view_eula_caption": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)", "screenshare_button_label": "Поділитися екраном", "settings": { - "developer_settings_label": "Налаштування розробника", - "developer_settings_label_description": "Відкрийте налаштування розробника у вікні налаштувань.", "developer_tab_title": "Розробнику", "feedback_tab_body": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.", "feedback_tab_description_label": "Ваш відгук", @@ -108,7 +106,6 @@ "feedback_tab_send_logs_label": "Долучити журнали налагодження", "feedback_tab_thank_you": "Дякуємо, ми отримали ваш відгук!", "feedback_tab_title": "Відгук", - "more_tab_title": "Докладніше", "opt_in_description": "<0><1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику.", "speaker_device_selection_label": "Динамік" }, diff --git a/locales/vi/app.json b/locales/vi/app.json index 2e59af69..3bd622ce 100644 --- a/locales/vi/app.json +++ b/locales/vi/app.json @@ -55,7 +55,6 @@ "register_confirm_password_label": "Xác nhận mật khẩu", "screenshare_button_label": "Chia sẻ màn hình", "settings": { - "developer_settings_label": "Cài đặt phát triển", "developer_tab_title": "Nhà phát triển", "feedback_tab_description_label": "Phản hồi của bạn", "feedback_tab_h4": "Gửi phản hồi", diff --git a/locales/zh-Hans/app.json b/locales/zh-Hans/app.json index da450f15..58d405d0 100644 --- a/locales/zh-Hans/app.json +++ b/locales/zh-Hans/app.json @@ -92,8 +92,6 @@ "room_auth_view_eula_caption": "点击 \"加入通话\",即表示您同意我们的<2>最终用户许可协议 (EULA)", "screenshare_button_label": "屏幕共享", "settings": { - "developer_settings_label": "开发者设置", - "developer_settings_label_description": "在设置中显示开发者设置。", "developer_tab_title": "开发者", "feedback_tab_body": "如果遇到问题或想提供一些反馈意见,请在下面向我们发送简短描述。", "feedback_tab_description_label": "您的反馈", @@ -101,7 +99,6 @@ "feedback_tab_send_logs_label": "包含调试日志", "feedback_tab_thank_you": "谢谢,我们收到了反馈!", "feedback_tab_title": "反馈", - "more_tab_title": "更多", "opt_in_description": "<0><1>您可以取消选中复选框来撤回同意。如果正在通话中,此设置将在通话结束时生效。", "speaker_device_selection_label": "发言人" }, diff --git a/locales/zh-Hant/app.json b/locales/zh-Hant/app.json index f6bb2aa0..b73e4658 100644 --- a/locales/zh-Hant/app.json +++ b/locales/zh-Hant/app.json @@ -99,8 +99,6 @@ "room_auth_view_eula_caption": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)", "screenshare_button_label": "分享畫面", "settings": { - "developer_settings_label": "開發者設定", - "developer_settings_label_description": "在設定視窗中顯示開發者設定。", "developer_tab_title": "開發者", "feedback_tab_body": "若您遇到問題或只是想提供一些回饋,請在下方傳送簡短說明給我們。", "feedback_tab_description_label": "您的回饋", @@ -108,7 +106,6 @@ "feedback_tab_send_logs_label": "包含除錯紀錄", "feedback_tab_thank_you": "感謝,我們已經收到您的回饋了!", "feedback_tab_title": "回饋", - "more_tab_title": "更多", "opt_in_description": "<0><1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。", "speaker_device_selection_label": "發言者" }, From a723f10d2cdb71d25725bb6d2d2ac628b4f2ead0 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 13 Dec 2024 14:53:08 +0000 Subject: [PATCH 09/19] Developer setting to show LiveKit participants that do not have MatrixRTC sessions a.k.a. non-member tiles (#2771) * make tiles based on rtc member * display missing lk participant + fix tile multiplier * add show_non_member_participants config option * per member tiles * merge fixes * linter * linter and tests * tests * adapt tests (wip) * Remove unused keys * Fix optionality of nonMemberItemCount * video is optional * Mock RTC members * Lint * Merge fixes * Fix user id * Add explicit types for public fields * isRTCParticipantAvailable => isLiveKitParticipantAvailable * isLiveKitParticipantAvailable * Readonly * More keys removal * Make local field based on view model class not observable * Wording * Fix RTC members in tes * Tests again * Lint * Disable showing non-member tiles by default * Duplicate screen sharing tiles like we used to * Lint * Revert function reordering * Remove throttleTime from bad merge * Cleanup * Tidy config of show non-member settings * tidy up handling of local rtc member in tests * tidy up test init * Fix mocks * Cleanup * Apply local override where participant not yet known * Handle no visible media id * Assertions for one-on-one view * Remove isLiveKitParticipantAvailable and show via encryption status * Handle no local media (yet) * Remove unused effect for setting * Tidy settings * Avoid case of one-to-one layout with missing local or remote * Iterate * Remove option to show non-member tiles to simplify code review * Remove unused code * Remove more remnants of show-non-member-tiles * iterate * back * Fix unit test * Refactor * Expose TestScheduler as global * Fix incorrect type assertion * Simplify speaking observer * Fix * Whitespace * Make it clear that we are mocking MatrixRTC memberships * Test case for only showing tiles for MatrixRTC session members * Simplify diff * Simplify diff These changes are in https://github.com/element-hq/element-call/pull/2809 * . * Whitespaces * Use asObservable when exposing subject * Show "waiting for media..." when no participant * Additional test case * Don't show "waiting for media..." in case of local participant * Make the loading state more subtle - instead of a label we show a animated gradient * Use correct key for matrix rtc foci in code comment. (#2838) * Update src/tile/SpotlightTile.tsx Co-authored-by: Timo <16718859+toger5@users.noreply.github.com> * Update src/state/CallViewModel.ts Co-authored-by: Timo <16718859+toger5@users.noreply.github.com> * Make the purpose of BaseMediaViewModel.local explicit * Use named object instead of unnamed array for spotlightAndPip * Refactor spotlightAndPip into spotlight and pip * Use if statement instead of ternary for readability in spotlight and pip logic * Review feedback * Fix tests for CallEventAudioRenderer * Lint * Developer setting to show non-member tiles This is based on top of https://github.com/element-hq/element-call/pull/2701 * Lint * Remove unused code * Remove changes that should be in https://github.com/element-hq/element-call/pull/2858 * Update src/state/CallViewModel.test.ts Co-authored-by: Robin * Merge branch 'livekit' into toger5/show-non-member-tiles * Remove unused nonMemberItemCount * Restore default showNonMemberTiles value after test * Update comments --------- Co-authored-by: Timo Co-authored-by: Timo <16718859+toger5@users.noreply.github.com> Co-authored-by: Robin --- locales/en/app.json | 3 +- src/settings/DeveloperSettingsTab.tsx | 18 ++++++++ src/settings/settings.ts | 4 ++ src/state/CallViewModel.test.ts | 48 ++++++++++++++++++++ src/state/CallViewModel.ts | 64 +++++++++++++++++++++++++-- 5 files changed, 133 insertions(+), 4 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 84038c2b..33f8f921 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -74,7 +74,8 @@ "device_id": "Device ID: {{id}}", "duplicate_tiles_label": "Number of additional tile copies per participant", "hostname": "Hostname: {{hostname}}", - "matrix_id": "Matrix ID: {{id}}" + "matrix_id": "Matrix ID: {{id}}", + "show_non_member_tiles": "Show tiles for non-member media" }, "disconnected_banner": "Connectivity to the server has been lost.", "full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.", diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index 209bc41e..057b0b0c 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -13,6 +13,7 @@ import { useSetting, duplicateTiles as duplicateTilesSetting, debugTileLayout as debugTileLayoutSetting, + showNonMemberTiles as showNonMemberTilesSetting, } from "./settings"; import type { MatrixClient } from "matrix-js-sdk/src/client"; @@ -26,6 +27,9 @@ export const DeveloperSettingsTab: FC = ({ client }) => { const [debugTileLayout, setDebugTileLayout] = useSetting( debugTileLayoutSetting, ); + const [showNonMemberTiles, setShowNonMemberTiles] = useSetting( + showNonMemberTilesSetting, + ); return ( <> @@ -85,6 +89,20 @@ export const DeveloperSettingsTab: FC = ({ client }) => { } /> + + ): void => { + setShowNonMemberTiles(event.target.checked); + }, + [setShowNonMemberTiles], + )} + /> + ); }; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index e8dda379..a902f9ab 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -72,6 +72,10 @@ export const developerMode = new Setting("developer-settings-tab", false); export const duplicateTiles = new Setting("duplicate-tiles", 0); +export const showNonMemberTiles = new Setting( + "show-non-member-tiles", + false, +); export const debugTileLayout = new Setting("debug-tile-layout", false); export const audioInput = new Setting( diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel.test.ts index 0e45faa1..d5b84d49 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel.test.ts @@ -46,6 +46,7 @@ import { type ECConnectionState, } from "../livekit/useECConnectionState"; import { E2eeType } from "../e2ee/e2eeType"; +import { showNonMemberTiles } from "../settings/settings"; vi.mock("@livekit/components-core"); @@ -686,6 +687,53 @@ test("participants must have a MatrixRTCSession to be visible", () => { }); }); +test("shows participants without MatrixRTCSession when enabled in settings", () => { + try { + // enable the setting: + showNonMemberTiles.setValue(true); + withTestScheduler(({ hot, expectObservable }) => { + const scenarioInputMarbles = " abc"; + const expectedLayoutMarbles = "abc"; + + withCallViewModel( + hot(scenarioInputMarbles, { + a: [], + b: [aliceParticipant], + c: [aliceParticipant, bobParticipant], + }), + of([]), // No one joins the MatrixRTC session + of(ConnectionState.Connected), + new Map(), + (vm) => { + vm.setGridMode("grid"); + expectObservable(summarizeLayout(vm.layout)).toBe( + expectedLayoutMarbles, + { + a: { + type: "grid", + spotlight: undefined, + grid: ["local:0"], + }, + b: { + type: "one-on-one", + local: "local:0", + remote: `${aliceId}:0`, + }, + c: { + type: "grid", + spotlight: undefined, + grid: ["local:0", `${aliceId}:0`, `${bobId}:0`], + }, + }, + ); + }, + ); + }); + } finally { + showNonMemberTiles.setValue(showNonMemberTiles.defaultValue); + } +}); + it("should show at least one tile per MatrixRTCSession", () => { withTestScheduler(({ hot, expectObservable }) => { // iterate through some combinations of MatrixRTC memberships diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 108a44c9..c701519b 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -69,7 +69,7 @@ import { } from "./MediaViewModel"; import { accumulate, finalizeValue } from "../utils/observable"; import { ObservableScope } from "./ObservableScope"; -import { duplicateTiles } from "../settings/settings"; +import { duplicateTiles, showNonMemberTiles } from "../settings/settings"; import { isFirefox } from "../Platform"; import { setPipEnabled } from "../controls"; import { @@ -449,6 +449,7 @@ export class CallViewModel extends ViewModel { this.matrixRTCSession, MatrixRTCSessionEvent.MembershipsChanged, ).pipe(startWith(null)), + showNonMemberTiles.value, ]).pipe( scan( ( @@ -458,6 +459,7 @@ export class CallViewModel extends ViewModel { { participant: localParticipant }, duplicateTiles, _membershipsChanged, + showNonMemberTiles, ], ) => { const newItems = new Map( @@ -495,9 +497,17 @@ export class CallViewModel extends ViewModel { } for (let i = 0; i < 1 + duplicateTiles; i++) { const indexedMediaId = `${livekitParticipantId}:${i}`; - const prevMedia = prevItems.get(indexedMediaId); + let prevMedia = prevItems.get(indexedMediaId); if (prevMedia && prevMedia instanceof UserMedia) { prevMedia.updateParticipant(participant); + if (prevMedia.vm.member === undefined) { + // We have a previous media created because of the `debugShowNonMember` flag. + // In this case we actually replace the media item. + // This "hack" never occurs if we do not use the `debugShowNonMember` debugging + // option and if we always find a room member for each rtc member (which also + // only fails if we have a fundamental problem) + prevMedia = undefined; + } } yield [ indexedMediaId, @@ -533,7 +543,55 @@ export class CallViewModel extends ViewModel { }.bind(this)(), ); - return newItems; + // Generate non member items (items without a corresponding MatrixRTC member) + // Those items should not be rendered, they are participants in LiveKit that do not have a corresponding + // MatrixRTC members. This cannot be any good: + // - A malicious user impersonates someone + // - Someone injects abusive content + // - The user cannot have encryption keys so it makes no sense to participate + // We can only trust users that have a MatrixRTC member event. + // + // This is still available as a debug option. This can be useful + // - If one wants to test scalability using the LiveKit CLI. + // - If an experimental project does not yet do the MatrixRTC bits. + // - If someone wants to debug if the LiveKit connection works but MatrixRTC room state failed to arrive. + const newNonMemberItems = showNonMemberTiles + ? new Map( + function* (this: CallViewModel): Iterable<[string, MediaItem]> { + for (const participant of remoteParticipants) { + for (let i = 0; i < 1 + duplicateTiles; i++) { + const maybeNonMemberParticipantId = + participant.identity + ":" + i; + if (!newItems.has(maybeNonMemberParticipantId)) { + const nonMemberId = maybeNonMemberParticipantId; + yield [ + nonMemberId, + prevItems.get(nonMemberId) ?? + new UserMedia( + nonMemberId, + undefined, + participant, + this.encryptionSystem, + this.livekitRoom, + ), + ]; + } + } + } + }.bind(this)(), + ) + : new Map(); + if (newNonMemberItems.size > 0) { + logger.debug("Added NonMember items: ", newNonMemberItems); + } + + const combinedNew = new Map([ + ...newNonMemberItems.entries(), + ...newItems.entries(), + ]); + + for (const [id, t] of prevItems) if (!combinedNew.has(id)) t.destroy(); + return combinedNew; }, new Map(), ), From de276b1fc33eca2a321ce6050c676bda5514552c Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 13 Dec 2024 15:22:44 -0500 Subject: [PATCH 10/19] Annotate the default device with a label --- locales/en/app.json | 1 + src/livekit/MediaDevicesContext.tsx | 7 ++- src/settings/DeviceSelection.module.css | 4 ++ src/settings/DeviceSelection.tsx | 75 +++++++++++++++++-------- 4 files changed, 62 insertions(+), 25 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 8e963ec1..a47e5beb 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -155,6 +155,7 @@ "camera": "Camera", "camera_numbered": "Camera {{n}}", "default": "Default", + "default_named": "Default <2>({{name}})", "microphone": "Microphone", "microphone_numbered": "Microphone {{n}}", "speaker": "Speaker", diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 513e6b2f..82b06bb0 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -31,7 +31,7 @@ import { export type DeviceLabel = | { type: "name"; name: string } | { type: "number"; number: number } - | { type: "default" }; + | { type: "default"; name: string | null }; export interface MediaDevice { /** @@ -104,7 +104,10 @@ function useMediaDevice( !available.has("") && !available.has("default") ) - available = new Map([["", { type: "default" }], ...available]); + available = new Map([ + ["", { type: "default", name: availableRaw[0]?.label || null }], + ...available, + ]); // Note: creating virtual default input devices would be another problem // entirely, because requesting a media stream from deviceId "" won't // automatically track the default device. diff --git a/src/settings/DeviceSelection.module.css b/src/settings/DeviceSelection.module.css index daa4510e..6686702f 100644 --- a/src/settings/DeviceSelection.module.css +++ b/src/settings/DeviceSelection.module.css @@ -16,3 +16,7 @@ flex-direction: column; gap: var(--cpd-space-4x); } + +.secondary { + color: var(--cpd-color-text-secondary); +} diff --git a/src/settings/DeviceSelection.tsx b/src/settings/DeviceSelection.tsx index 9faa1e82..aebe0aac 100644 --- a/src/settings/DeviceSelection.tsx +++ b/src/settings/DeviceSelection.tsx @@ -5,7 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { type ChangeEvent, type FC, useCallback, useId } from "react"; +import { + type ChangeEvent, + type FC, + type ReactElement, + type ReactNode, + useCallback, + useId, +} from "react"; import { Heading, InlineField, @@ -13,7 +20,7 @@ import { RadioControl, Separator, } from "@vector-im/compound-web"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { type MediaDevice } from "../livekit/MediaDevicesContext"; import styles from "./DeviceSelection.module.css"; @@ -53,27 +60,49 @@ export const DeviceSelection: FC = ({
- {[...devices.available].map(([id, label]) => ( - - } - > - - - ))} + {[...devices.available].map(([id, label]) => { + let labelText: ReactNode; + switch (label.type) { + case "name": + labelText = label.name; + break; + case "number": + labelText = numberedLabel(label.number); + break; + case "default": + labelText = + label.name === null ? ( + t("settings.devices.default") + ) : ( + + Default{" "} + + ({{ name: label.name } as unknown as ReactElement}) + + + ); + break; + } + + return ( + + } + > + + + ); + })}
); From f9e3fe31762504bc610d690e53aef3a4585ee0af Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 13 Dec 2024 15:37:29 -0500 Subject: [PATCH 11/19] Use observables for more of the media devices logic --- src/livekit/MediaDevicesContext.tsx | 83 +++++++++++++++++------------ 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 82b06bb0..9edbb884 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -16,7 +16,7 @@ import { useState, } from "react"; import { createMediaDeviceObserver } from "@livekit/components-core"; -import { startWith } from "rxjs"; +import { map, startWith } from "rxjs"; import { useObservableEagerState } from "observable-hooks"; import { logger } from "matrix-js-sdk/src/logger"; @@ -83,36 +83,43 @@ function useMediaDevice( ).pipe(startWith([])), [kind, requestPermissions], ); - const availableRaw = useObservableEagerState(deviceObserver); - const available = useMemo(() => { - // Sometimes browsers (particularly Firefox) can return multiple device - // entries for the exact same device ID; using a map deduplicates them - let available = new Map( - availableRaw.map((d, i) => [ - d.deviceId, - d.label - ? { type: "name", name: d.label } - : { type: "number", number: i + 1 }, - ]), - ); - // Create a virtual default audio output for browsers that don't have one. - // Its device ID must be the empty string because that's what setSinkId - // recognizes. - if ( - kind === "audiooutput" && - available.size && - !available.has("") && - !available.has("default") - ) - available = new Map([ - ["", { type: "default", name: availableRaw[0]?.label || null }], - ...available, - ]); - // Note: creating virtual default input devices would be another problem - // entirely, because requesting a media stream from deviceId "" won't - // automatically track the default device. - return available; - }, [kind, availableRaw]); + const available = useObservableEagerState( + useMemo( + () => + deviceObserver.pipe( + map((availableRaw) => { + // Sometimes browsers (particularly Firefox) can return multiple device + // entries for the exact same device ID; using a map deduplicates them + let available = new Map( + availableRaw.map((d, i) => [ + d.deviceId, + d.label + ? { type: "name", name: d.label } + : { type: "number", number: i + 1 }, + ]), + ); + // Create a virtual default audio output for browsers that don't have one. + // Its device ID must be the empty string because that's what setSinkId + // recognizes. + if ( + kind === "audiooutput" && + available.size && + !available.has("") && + !available.has("default") + ) + available = new Map([ + ["", { type: "default", name: availableRaw[0]?.label || null }], + ...available, + ]); + // Note: creating virtual default input devices would be another problem + // entirely, because requesting a media stream from deviceId "" won't + // automatically track the default device. + return available; + }), + ), + [kind, deviceObserver], + ), + ); const [preferredId, select] = useSetting(setting); const selectedId = useMemo(() => { @@ -130,9 +137,17 @@ function useMediaDevice( } return undefined; }, [available, preferredId]); - const selectedGroupId = useMemo( - () => availableRaw.find((d) => d.deviceId === selectedId)?.groupId, - [availableRaw, selectedId], + const selectedGroupId = useObservableEagerState( + useMemo( + () => + deviceObserver.pipe( + map( + (availableRaw) => + availableRaw.find((d) => d.deviceId === selectedId)?.groupId, + ), + ), + [deviceObserver, selectedId], + ), ); return useMemo( From 25d0338f35b2c354053085bac48dbaeedec42a3d Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 13 Dec 2024 16:40:20 -0500 Subject: [PATCH 12/19] Skip some redundant updates to the grid and spotlight While debugging our layout shift issue I learned that a single change to the sort order of the participants can cause 3 or 4 redundant emissions of the same items in the same order. Since each of these would cause React to re-render the grid, skipping these spurious emissions seems like an easy performance win. --- src/state/CallViewModel.ts | 4 ++++ src/utils/array.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/utils/array.ts diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index c701519b..8e080fdc 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -83,6 +83,7 @@ import { oneOnOneLayout } from "./OneOnOneLayout"; import { pipLayout } from "./PipLayout"; import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; import { observeSpeaker } from "./observeSpeaker"; +import { shallowEquals } from "../utils/array"; // How long we wait after a focus switch before showing the real participant // list again @@ -705,6 +706,8 @@ export class CallViewModel extends ViewModel { bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm), ); }), + distinctUntilChanged(shallowEquals), + this.scope.state(), ); private readonly spotlight: Observable = @@ -718,6 +721,7 @@ export class CallViewModel extends ViewModel { map((speaker) => (speaker ? [speaker] : [])), ); }), + distinctUntilChanged(shallowEquals), this.scope.state(), ); diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 00000000..70ecbd89 --- /dev/null +++ b/src/utils/array.ts @@ -0,0 +1,16 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +/** + * Determine whether two arrays are equal by shallow comparison. + */ +export function shallowEquals(first: A[], second: A[]): boolean { + if (first.length !== second.length) return false; + for (let i = 0; i < first.length; i++) + if (first[i] !== second[i]) return false; + return true; +} From 92afd5d63ae2914f651a3eef93554b3cbf4abb40 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 16 Dec 2024 11:22:23 +0000 Subject: [PATCH 13/19] Wait for .well-known/matrix/client to load before determining MatrixRTC foci (#2901) --- src/rtcSessionHelper.test.ts | 2 +- src/rtcSessionHelpers.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/rtcSessionHelper.test.ts b/src/rtcSessionHelper.test.ts index 7df9f1b3..729545f7 100644 --- a/src/rtcSessionHelper.test.ts +++ b/src/rtcSessionHelper.test.ts @@ -40,7 +40,7 @@ test("It joins the correct Session", async () => { room: { roomId: "roomId", client: { - getClientWellKnown: vi.fn().mockReturnValue(clientWellKnown), + waitForClientWellKnown: vi.fn().mockResolvedValue(clientWellKnown), }, }, memberships: [], diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index f1c7eb8c..680df571 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -44,8 +44,9 @@ async function makePreferredLivekitFoci( } // Prioritize the client well known over the configured sfu. - const wellKnownFoci = - rtcSession.room.client.getClientWellKnown()?.[FOCI_WK_KEY]; + const wellKnownFoci = ( + await rtcSession.room.client.waitForClientWellKnown() + )?.[FOCI_WK_KEY]; if (Array.isArray(wellKnownFoci)) { preferredFoci.push( ...wellKnownFoci From b822b9f80df3e306d576289abfea127802a3f09e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:44:57 +0100 Subject: [PATCH 14/19] Update all non-major dependencies (#2900) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 527 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 300 insertions(+), 227 deletions(-) diff --git a/yarn.lock b/yarn.lock index d607f394..5fdd147f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2340,6 +2340,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== +"@radix-ui/primitive@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3" + integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA== + "@radix-ui/react-arrow@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a" @@ -2357,11 +2362,26 @@ "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-slot" "1.1.0" +"@radix-ui/react-collection@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.1.tgz#be2c7e01d3508e6d4b6d838f492e7d182f17d3b0" + integrity sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-compose-refs@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== +"@radix-ui/react-compose-refs@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" + integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw== + "@radix-ui/react-context-menu@^2.2.1": version "2.2.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.2.tgz#efcddc559fc3011721b65148f062d04027f76c7a" @@ -2385,21 +2405,21 @@ integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q== "@radix-ui/react-dialog@^1.0.4", "@radix-ui/react-dialog@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz#d9345575211d6f2d13e209e84aec9a8584b54d6c" - integrity sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA== + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.3.tgz#87cf49f619a6a0f6219980678be0f7c31978dee1" + integrity sha512-ujGvqQNkZ0J7caQyl8XuZRj2/TIrYcOGwqz5TeD1OMcCdfBuEMP0D12ve+8J5F9XuNUth3FAKFWo/wt0E/GJrQ== dependencies: - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-dismissable-layer" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.2" "@radix-ui/react-focus-guards" "1.1.1" - "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-focus-scope" "1.1.1" "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-portal" "1.1.2" - "@radix-ui/react-presence" "1.1.1" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-portal" "1.1.3" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-slot" "1.1.1" "@radix-ui/react-use-controllable-state" "1.1.0" aria-hidden "^1.1.1" react-remove-scroll "2.6.0" @@ -2420,6 +2440,17 @@ "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-escape-keydown" "1.1.0" +"@radix-ui/react-dismissable-layer@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.2.tgz#771594b202f32bc8ffeb278c565f10c513814aee" + integrity sha512-kEHnlhv7wUggvhuJPkyw4qspXLJOdYoAP4dO2c8ngGuXTq1w/HZp1YeVB+NQ2KbH1iEG+pvOCGYSqh9HZOz6hg== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-escape-keydown" "1.1.0" + "@radix-ui/react-dropdown-menu@^2.1.1": version "2.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924" @@ -2447,6 +2478,15 @@ "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-callback-ref" "1.1.0" +"@radix-ui/react-focus-scope@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz#5c602115d1db1c4fcfa0fae4c3b09bb8919853cb" + integrity sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-form@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-form/-/react-form-0.1.0.tgz#7111a6aa54a2bde0d11fb72643f9ffc871ac58ad" @@ -2521,6 +2561,14 @@ "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-portal@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440" + integrity sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-presence@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz#98aba423dba5e0c687a782c0669dcd99de17f9b1" @@ -2529,6 +2577,14 @@ "@radix-ui/react-compose-refs" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-presence@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc" + integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-primitive@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884" @@ -2536,6 +2592,13 @@ dependencies: "@radix-ui/react-slot" "1.1.0" +"@radix-ui/react-primitive@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e" + integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg== + dependencies: + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-progress@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.0.tgz#28c267885ec154fc557ec7a66cb462787312f7e2" @@ -2567,17 +2630,17 @@ "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-slider@^1.1.2": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.2.1.tgz#acb0804309890f3cd7a224b2b0c4c4704f32921b" - integrity sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw== + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.2.2.tgz#4ca883e3f0dea7b97d43c6cbc6c4305c64e75a86" + integrity sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA== dependencies: "@radix-ui/number" "1.1.0" - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-collection" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-collection" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" "@radix-ui/react-context" "1.1.1" "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-primitive" "2.0.1" "@radix-ui/react-use-controllable-state" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0" "@radix-ui/react-use-previous" "1.1.0" @@ -2590,6 +2653,13 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.0" +"@radix-ui/react-slot@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3" + integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-use-callback-ref@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" @@ -2634,11 +2704,11 @@ "@radix-ui/react-use-layout-effect" "1.1.0" "@radix-ui/react-visually-hidden@^1.0.3": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz#ad47a8572580f7034b3807c8e6740cd41038a5a2" - integrity sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz#f7b48c1af50dfdc366e92726aee6d591996c5752" + integrity sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg== dependencies: - "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-primitive" "2.0.1" "@radix-ui/rect@1.1.0": version "1.1.0" @@ -2704,231 +2774,236 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz#3e7eda4c0c1de6d2415343002d742ff95e38dca7" integrity sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA== -"@rollup/rollup-android-arm-eabi@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz#462e7ecdd60968bc9eb95a20d185e74f8243ec1b" - integrity sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ== +"@rollup/rollup-android-arm-eabi@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz#7f4c4d8cd5ccab6e95d6750dbe00321c1f30791e" + integrity sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ== "@rollup/rollup-android-arm64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz#04f679231acf7284f1f8a1f7250d0e0944865ba8" integrity sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg== -"@rollup/rollup-android-arm64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz#78a2b8a8a55f71a295eb860a654ae90a2b168f40" - integrity sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA== +"@rollup/rollup-android-arm64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz#17ea71695fb1518c2c324badbe431a0bd1879f2d" + integrity sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA== "@rollup/rollup-darwin-arm64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz#ecea723041621747d0772af93b54752edf26467a" integrity sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg== -"@rollup/rollup-darwin-arm64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz#5b783af714f434f1e66e3cdfa3817e0b99216d84" - integrity sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q== +"@rollup/rollup-darwin-arm64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz#dac0f0d0cfa73e7d5225ae6d303c13c8979e7999" + integrity sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ== "@rollup/rollup-darwin-x64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz#28e6e0687092f31e20982fc104779d48c643fc21" integrity sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA== -"@rollup/rollup-darwin-x64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz#f72484e842521a5261978034e18e20f778a2850d" - integrity sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w== +"@rollup/rollup-darwin-x64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz#8f63baa1d31784904a380d2e293fa1ddf53dd4a2" + integrity sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ== "@rollup/rollup-freebsd-arm64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz#99e9173b8aef3d1ef086983da70413988206e530" integrity sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g== -"@rollup/rollup-freebsd-arm64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz#3c919dff72b2fe344811a609c674a8347b033f62" - integrity sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ== +"@rollup/rollup-freebsd-arm64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz#30ed247e0df6e8858cdc6ae4090e12dbeb8ce946" + integrity sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA== "@rollup/rollup-freebsd-x64@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz#f3a1ef941f8d3c6b2b036484c69a7b2d3d9ebbd7" integrity sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw== -"@rollup/rollup-freebsd-x64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz#b62a3a8365b363b3fdfa6da11a9188b6ab4dca7c" - integrity sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA== +"@rollup/rollup-freebsd-x64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz#57846f382fddbb508412ae07855b8a04c8f56282" + integrity sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ== "@rollup/rollup-linux-arm-gnueabihf@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz#9ba6adcc33f26f2a0c6ee658f0bbda4de8da2f75" integrity sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA== -"@rollup/rollup-linux-arm-gnueabihf@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz#0d02cc55bd229bd8ca5c54f65f916ba5e0591c94" - integrity sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w== +"@rollup/rollup-linux-arm-gnueabihf@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz#378ca666c9dae5e6f94d1d351e7497c176e9b6df" + integrity sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA== "@rollup/rollup-linux-arm-musleabihf@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz#62f2426fa9016ec884f4fa779d7b62d5ba02a41a" integrity sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ== -"@rollup/rollup-linux-arm-musleabihf@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz#c51d379263201e88a60e92bd8e90878f0c044425" - integrity sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg== +"@rollup/rollup-linux-arm-musleabihf@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz#a692eff3bab330d5c33a5d5813a090c15374cddb" + integrity sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg== "@rollup/rollup-linux-arm64-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz#f98ec111a231d35e0c6d3404e3d80f67f9d5b9f8" integrity sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A== -"@rollup/rollup-linux-arm64-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz#93ce2addc337b5cfa52b84f8e730d2e36eb4339b" - integrity sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg== +"@rollup/rollup-linux-arm64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz#6b1719b76088da5ac1ae1feccf48c5926b9e3db9" + integrity sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA== "@rollup/rollup-linux-arm64-musl@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz#4b36ffb8359f959f2c29afd187603c53368b6723" integrity sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw== -"@rollup/rollup-linux-arm64-musl@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz#730af6ddc091a5ba5baac28a3510691725dc808b" - integrity sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw== +"@rollup/rollup-linux-arm64-musl@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz#865baf5b6f5ff67acb32e5a359508828e8dc5788" + integrity sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A== + +"@rollup/rollup-linux-loongarch64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz#23c6609ba0f7fa7a7f2038b6b6a08555a5055a87" + integrity sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA== "@rollup/rollup-linux-powerpc64le-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz#52f4b39e6783505d168a745b79d86474fde71680" integrity sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA== -"@rollup/rollup-linux-powerpc64le-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz#b5565aac20b4de60ca1e557f525e76478b5436af" - integrity sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ== +"@rollup/rollup-linux-powerpc64le-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz#652ef0d9334a9f25b9daf85731242801cb0fc41c" + integrity sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A== "@rollup/rollup-linux-riscv64-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz#49195be7e6a7d68d482b12461e2ea914e31ff977" integrity sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA== -"@rollup/rollup-linux-riscv64-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz#d488290bf9338bad4ae9409c4aa8a1728835a20b" - integrity sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g== +"@rollup/rollup-linux-riscv64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz#1eb6651839ee6ebca64d6cc64febbd299e95e6bd" + integrity sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA== "@rollup/rollup-linux-s390x-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz#4b8d50a205eac7b46cdcb9c50d4a6ae5994c02e0" integrity sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ== -"@rollup/rollup-linux-s390x-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz#eb2e3f3a06acf448115045c11a5a96868c95a556" - integrity sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw== +"@rollup/rollup-linux-s390x-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz#015c52293afb3ff2a293cf0936b1d43975c1e9cd" + integrity sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg== "@rollup/rollup-linux-x64-gnu@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz#dfcceebc5ccac7fc2db19471996026258c81b55f" integrity sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig== -"@rollup/rollup-linux-x64-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz#065952ef2aea7e837dc7e02aa500feeaff4fc507" - integrity sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw== +"@rollup/rollup-linux-x64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz#b83001b5abed2bcb5e2dbeec6a7e69b194235c1e" + integrity sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw== "@rollup/rollup-linux-x64-musl@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz#192f78bad8429711d63a31dc0a7d3312e2df850e" integrity sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ== -"@rollup/rollup-linux-x64-musl@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz#3435d484d05f5c4d1ffd54541b4facce2887103a" - integrity sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw== +"@rollup/rollup-linux-x64-musl@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz#6cc7c84cd4563737f8593e66f33b57d8e228805b" + integrity sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g== "@rollup/rollup-win32-arm64-msvc@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz#f4ec076579634f780b4e5896ae7f59f3e38e0c60" integrity sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww== -"@rollup/rollup-win32-arm64-msvc@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz#69682a2a10d9fedc334f87583cfca83c39c08077" - integrity sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg== +"@rollup/rollup-win32-arm64-msvc@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz#631ffeee094d71279fcd1fe8072bdcf25311bc11" + integrity sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A== "@rollup/rollup-win32-ia32-msvc@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz#5458eab1929827e4f805cefb90bd09ecf7eeed2b" integrity sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg== -"@rollup/rollup-win32-ia32-msvc@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz#b64470f9ac79abb386829c56750b9a4711be3332" - integrity sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A== +"@rollup/rollup-win32-ia32-msvc@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz#06d1d60d5b9f718e8a6c4a43f82e3f9e3254587f" + integrity sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA== "@rollup/rollup-win32-x64-msvc@4.25.0": version "4.25.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz#93415e7e707e4b156d77c5950b983b58f4bc33f3" integrity sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg== -"@rollup/rollup-win32-x64-msvc@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz#cb313feef9ac6e3737067fdf34f42804ac65a6f2" - integrity sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ== +"@rollup/rollup-win32-x64-msvc@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz#4dff5c4259ebe6c5b4a8f2c5bc3829b7a8447ff0" + integrity sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA== "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sentry-internal/browser-utils@8.43.0": - version "8.43.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.43.0.tgz#b064908a537d1cc17d8ddaf0f4c5d712557cbf40" - integrity sha512-5WhJZ3SA5sZVDBwOsChDd5JCzYcwBX7sEqBqEcm3pFru6TUihEnFIJmDIbreIyrQMwUhs3dTxnfnidgjr5z1Ag== +"@sentry-internal/browser-utils@8.45.0": + version "8.45.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.45.0.tgz#8e9217b8e8a4242c9a8244dce648289eaa1e38a0" + integrity sha512-MX/E/C+W5I9jkGD1PsbZ2hpCc7YuizNKmEbuGPxQPfUSIPrdE2wpo6ZfIhEbxq9m/trl1oRCN4PXi3BB7dlYYg== dependencies: - "@sentry/core" "8.43.0" + "@sentry/core" "8.45.0" -"@sentry-internal/feedback@8.43.0": - version "8.43.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.43.0.tgz#9477b999c9bca62335eb944a6f7246a96beb0111" - integrity sha512-rcGR2kzFu4vLXBQbI9eGJwjyToyjl36O2q/UKbiZBNJ5IFtDvKRLke6jIHq/YqiHPfFGpVtq5M/lYduDfA/eaQ== +"@sentry-internal/feedback@8.45.0": + version "8.45.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.45.0.tgz#cfd7f54d5089682a2768c1229a5efcda4d9561fe" + integrity sha512-WerpfkKrKPAlnQuqjEgKXZtrx68cla7GyOkNOeL40JQbY4/By4Qjx1atUOmgk/FdjrCLPw+jQQY9pXRpMRqqRw== dependencies: - "@sentry/core" "8.43.0" + "@sentry/core" "8.45.0" -"@sentry-internal/replay-canvas@8.43.0": - version "8.43.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.43.0.tgz#f5672a08c9eb588afa0bf36f07b9f5c29b5c9920" - integrity sha512-rL8G7E1GtozH8VNalRrBQNjYDJ5ChWS/vpQI5hUG11PZfvQFXEVatLvT3uO2l0xIlHm4idTsHOSLTe/usxnogQ== +"@sentry-internal/replay-canvas@8.45.0": + version "8.45.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.45.0.tgz#46f39402ff0cfee4ae05191af20b4e4fac6f474c" + integrity sha512-LZ8kBuzO5gutDiWnCyYEzBMDLq9PIllcsWsXRpKoau0Zqs3DbyRolI11dNnxmUSh7UW21FksxBpqn5yPmUMbag== dependencies: - "@sentry-internal/replay" "8.43.0" - "@sentry/core" "8.43.0" + "@sentry-internal/replay" "8.45.0" + "@sentry/core" "8.45.0" -"@sentry-internal/replay@8.43.0": - version "8.43.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.43.0.tgz#4e2e3844f52b47b16bf816d21857921bbfe85d62" - integrity sha512-geV5/zejLfGGwWHjylzrb1w8NI3U37GMG9/53nmv13FmTXUDF5XF2lh41KXFVYwvp7Ha4bd1FRQ9IU9YtBWskw== +"@sentry-internal/replay@8.45.0": + version "8.45.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.45.0.tgz#e94d250de235491888694f7cf0f637114adb4b9a" + integrity sha512-SOFwFpzx0B6lxhLl2hBnxvybo7gdB5TMY8dOHMwXgk5A2+BXvSpvWXnr33yqUlBmC8R3LeFTB3C0plzM5lhkJg== dependencies: - "@sentry-internal/browser-utils" "8.43.0" - "@sentry/core" "8.43.0" + "@sentry-internal/browser-utils" "8.45.0" + "@sentry/core" "8.45.0" "@sentry/babel-plugin-component-annotate@2.22.7": version "2.22.7" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.7.tgz#604c7e33d48528a13477e7af597c4d5fca51b8bd" integrity sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ== -"@sentry/browser@8.43.0": - version "8.43.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.43.0.tgz#4eec67bc6fb278727304045b612ac392674cade6" - integrity sha512-LGvLLnfmR8+AEgFmd7Q7KHiOTiV0P1Lvio2ENDELhEqJOIiICauttibVmig+AW02qg4kMeywvleMsUYaZv2RVA== +"@sentry/browser@8.45.0": + version "8.45.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.45.0.tgz#2e8f7b8b1a7860863aae4d716b9748a21789f0e0" + integrity sha512-Y+BcfpXY1eEkOYOzgLGkx1YH940uMAymYOxfSZSvC+Vx6xHuaGT05mIFef/aeZbyu2AUs6JjdvD1BRBZlHg78w== dependencies: - "@sentry-internal/browser-utils" "8.43.0" - "@sentry-internal/feedback" "8.43.0" - "@sentry-internal/replay" "8.43.0" - "@sentry-internal/replay-canvas" "8.43.0" - "@sentry/core" "8.43.0" + "@sentry-internal/browser-utils" "8.45.0" + "@sentry-internal/feedback" "8.45.0" + "@sentry-internal/replay" "8.45.0" + "@sentry-internal/replay-canvas" "8.45.0" + "@sentry/core" "8.45.0" "@sentry/bundler-plugin-core@2.22.7": version "2.22.7" @@ -2998,18 +3073,18 @@ "@sentry/cli-win32-i686" "2.39.1" "@sentry/cli-win32-x64" "2.39.1" -"@sentry/core@8.43.0": - version "8.43.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.43.0.tgz#e96a489e87a9999199f5ac27d8860da37c1fa8b4" - integrity sha512-ktyovtjkTMNud+kC/XfqHVCoQKreIKgx/hgeRvzPwuPyd1t1KzYmRL3DBkbcWVnyOPpVTHn+RsEI1eRcVYHtvw== +"@sentry/core@8.45.0": + version "8.45.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.45.0.tgz#a03a1b666989898ce7fb33f9ec279ea08450b317" + integrity sha512-4YTuBipWSh4JrtSYS5GxUQBAcAgOIkEoFfFbwVcr3ivijOacJLRXTBn3rpcy1CKjBq0PHDGR+2RGRYC+bNAMxg== "@sentry/react@^8.0.0": - version "8.43.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.43.0.tgz#ad49bd16b0b1897613ef5cbd2f0a49b2b41f98a9" - integrity sha512-PsTzLrYio/FOJU537Y5Gj9jJi7OMHEjdttsC9INUxy5062LOd8ObtHsjE0mopLaSYEwUfSROQOBZCwmISh8ByQ== + version "8.45.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.45.0.tgz#9a1bfbbbb3575fffb92796acc28ad5bb93a6855a" + integrity sha512-xuJBDATJKAHOxpR5IBfGFWJxXb05GMPGGpk8UoWai1Mh50laAQ0/WW+5sDAKrCjXoA+JZ6fb3DP8EE2X93n1nw== dependencies: - "@sentry/browser" "8.43.0" - "@sentry/core" "8.43.0" + "@sentry/browser" "8.45.0" + "@sentry/core" "8.45.0" hoist-non-react-statics "^3.3.2" "@sentry/vite-plugin@^2.0.0": @@ -3273,9 +3348,9 @@ undici-types "~6.19.8" "@types/node@^22.0.0": - version "22.10.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.1.tgz#41ffeee127b8975a05f8c4f83fb89bcb2987d766" - integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== dependencies: undici-types "~6.20.0" @@ -3302,9 +3377,9 @@ "@types/node" "*" "@types/react-dom@^18.3.0": - version "18.3.3" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.3.tgz#3654138d0da1b0c7916f6ed0dc1cc2b576d47650" - integrity sha512-uTYkxTLkYp41nq/ULXyXMtkNT1vu5fXJoqad6uTNCOGat5t9cLgF4vMNLBXsTOXpdOI44XzKPY1M5RRm0bQHuw== + version "18.3.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.5.tgz#45f9f87398c5dcea085b715c58ddcf1faf65f716" + integrity sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q== "@types/react-router-dom@^5.3.3": version "5.3.3" @@ -4040,7 +4115,17 @@ broccoli-plugin@^4.0.7: rimraf "^3.0.2" symlink-or-copy "^1.3.1" -browserslist@^4.23.1, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2: +browserslist@^4.23.1, browserslist@^4.23.3: + version "4.24.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2" + integrity sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" + +browserslist@^4.24.0, browserslist@^4.24.2: version "4.24.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== @@ -4106,15 +4191,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001646: - version "1.0.30001680" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz#5380ede637a33b9f9f1fc6045ea99bd142f3da5e" - integrity sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA== - -caniuse-lite@^1.0.30001669: - version "1.0.30001687" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz#d0ac634d043648498eedf7a3932836beba90ebae" - integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ== +caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669, caniuse-lite@^1.0.30001688: + version "1.0.30001688" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz#f9d3ede749f083ce0db4c13db9d828adaf2e8d0a" + integrity sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA== caseless@~0.12.0: version "0.12.0" @@ -4405,10 +4485,10 @@ css-blank-pseudo@^7.0.1: dependencies: postcss-selector-parser "^7.0.0" -css-has-pseudo@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.1.tgz#adbb51821e51f7a7c1d2df4d12827870cc311137" - integrity sha512-EOcoyJt+OsuKfCADgLT7gADZI5jMzIe/AeI6MeAYKiFBDmNmM7kk46DtSfMj5AohUJisqVzopBpnQTlvbyaBWg== +css-has-pseudo@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz#fb42e8de7371f2896961e1f6308f13c2c7019b72" + integrity sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ== dependencies: "@csstools/selector-specificity" "^5.0.0" postcss-selector-parser "^7.0.0" @@ -4440,10 +4520,10 @@ css.escape@^1.5.1: resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== -cssdb@^8.2.1: - version "8.2.1" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.1.tgz#62a5d9a41e2c86f1d7c35981098fc5ce47c5766c" - integrity sha512-KwEPys7lNsC8OjASI8RrmwOYYDcm0JOW9zQhcV83ejYcQkirTEyeAGui8aO2F5PiS6SLpxuTzl6qlMElIdsgIg== +cssdb@^8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.3.tgz#7e6980bb5a785a9b4eb2a21bd38d50624b56cb46" + integrity sha512-9BDG5XmJrJQQnJ51VFxXCAtpZ5ebDlAREmO8sxMOVU0aSxN/gocbctjIG5LMh3WBUq+xTlb/jw2LoljBEqraTA== cssesc@^3.0.0: version "3.0.0" @@ -4703,10 +4783,10 @@ easy-table@1.2.0: optionalDependencies: wcwidth "^1.0.1" -electron-to-chromium@^1.5.41: - version "1.5.72" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz#a732805986d3a5b5fedd438ddf4616c7d78ac2df" - integrity sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw== +electron-to-chromium@^1.5.41, electron-to-chromium@^1.5.73: + version "1.5.73" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz#f32956ce40947fa3c8606726a96cd8fb5bb5f720" + integrity sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg== emoji-regex@^8.0.0: version "8.0.0" @@ -5867,13 +5947,6 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" @@ -6294,9 +6367,9 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== knip@^5.27.2: - version "5.39.2" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.39.2.tgz#1faacd8d8ef36b509b2f6e396cce85b645abb04e" - integrity sha512-BuvuWRllLWV/r2G4m9ggNH+DZ6gouP/dhtJPXVlMbWNF++w9/EfrF6k2g7YBKCwjzCC+PXmYtpH8S2t8RjnY4Q== + version "5.40.0" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.40.0.tgz#6da9113d9d0c696fc3e5dc3f3a281db57b4b828a" + integrity sha512-EzBfQDz4YBzYnMLueWnaaVr15mneqZs1c3RanttciuVuRcodlNjzAmR2nch/khlRdVABAxAdMGFxfSvhvcH1NA== dependencies: "@nodelib/fs.walk" "1.2.8" "@snyk/github-codeowners" "1.1.0" @@ -6414,7 +6487,7 @@ long@^5.0.0: resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -6603,9 +6676,9 @@ ms@^2.1.1, ms@^2.1.3: integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== natural-compare@^1.4.0: version "1.4.0" @@ -6632,7 +6705,7 @@ node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-releases@^2.0.18: +node-releases@^2.0.18, node-releases@^2.0.19: version "2.0.19" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== @@ -7133,9 +7206,9 @@ postcss-place@^10.0.0: postcss-value-parser "^4.2.0" postcss-preset-env@^10.0.0: - version "10.1.1" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.1.1.tgz#6ee631272353fb1c4a9711943e9b80a178ffce44" - integrity sha512-wqqsnBFD6VIwcHHRbhjTOcOi4qRVlB26RwSr0ordPj7OubRRxdWebv/aLjKLRR8zkZrbxZyuus03nOIgC5elMQ== + version "10.1.2" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.1.2.tgz#ea9c25d92045ef06edd78f9945d2586107aab3e3" + integrity sha512-OqUBZ9ByVfngWhMNuBEMy52Izj07oIFA6K/EOGBlaSv+P12MiE1+S2cqXtS1VuW82demQ/Tzc7typYk3uHunkA== dependencies: "@csstools/postcss-cascade-layers" "^5.0.1" "@csstools/postcss-color-function" "^4.0.6" @@ -7172,9 +7245,9 @@ postcss-preset-env@^10.0.0: autoprefixer "^10.4.19" browserslist "^4.23.1" css-blank-pseudo "^7.0.1" - css-has-pseudo "^7.0.1" + css-has-pseudo "^7.0.2" css-prefers-color-scheme "^10.0.0" - cssdb "^8.2.1" + cssdb "^8.2.3" postcss-attribute-case-insensitive "^7.0.1" postcss-clamp "^4.1.0" postcss-color-functional-notation "^7.0.6" @@ -7386,9 +7459,9 @@ react-error-boundary@^3.1.0: "@babel/runtime" "^7.12.5" react-i18next@^15.0.0: - version "15.1.4" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.4.tgz#65c03c31a5e42202000652e163f22f23a9306a60" - integrity sha512-2tai71gmehbvl9ZIqPMqlCCkm/cbeV1G4STpmM3C8Uzo6T2l8jDvZxEVSsQKt8blP9X34iRFP/k1ROqG2296MQ== + version "15.2.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.2.0.tgz#6b51650e1e93eb4d235a4d533fcf61b3bbf4ea10" + integrity sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg== dependencies: "@babel/runtime" "^7.25.0" html-parse-stringify "^3.0.1" @@ -7409,11 +7482,11 @@ react-refresh@^0.14.2: integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== react-remove-scroll-bar@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" - integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== + version "2.3.8" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" + integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== dependencies: - react-style-singleton "^2.2.1" + react-style-singleton "^2.2.2" tslib "^2.0.0" react-remove-scroll@2.6.0: @@ -7455,13 +7528,12 @@ react-router@5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-style-singleton@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" - integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== +react-style-singleton@^2.2.1, react-style-singleton@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" + integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== dependencies: get-nonce "^1.0.0" - invariant "^2.2.4" tslib "^2.0.0" react-use-clipboard@^1.0.7: @@ -7753,30 +7825,31 @@ rollup@^4.20.0: fsevents "~2.3.2" rollup@^4.23.0: - version "4.28.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.0.tgz#eb8d28ed43ef60a18f21d0734d230ee79dd0de77" - integrity sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ== + version "4.28.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.1.tgz#7718ba34d62b449dfc49adbfd2f312b4fe0df4de" + integrity sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg== dependencies: "@types/estree" "1.0.6" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.28.0" - "@rollup/rollup-android-arm64" "4.28.0" - "@rollup/rollup-darwin-arm64" "4.28.0" - "@rollup/rollup-darwin-x64" "4.28.0" - "@rollup/rollup-freebsd-arm64" "4.28.0" - "@rollup/rollup-freebsd-x64" "4.28.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.28.0" - "@rollup/rollup-linux-arm-musleabihf" "4.28.0" - "@rollup/rollup-linux-arm64-gnu" "4.28.0" - "@rollup/rollup-linux-arm64-musl" "4.28.0" - "@rollup/rollup-linux-powerpc64le-gnu" "4.28.0" - "@rollup/rollup-linux-riscv64-gnu" "4.28.0" - "@rollup/rollup-linux-s390x-gnu" "4.28.0" - "@rollup/rollup-linux-x64-gnu" "4.28.0" - "@rollup/rollup-linux-x64-musl" "4.28.0" - "@rollup/rollup-win32-arm64-msvc" "4.28.0" - "@rollup/rollup-win32-ia32-msvc" "4.28.0" - "@rollup/rollup-win32-x64-msvc" "4.28.0" + "@rollup/rollup-android-arm-eabi" "4.28.1" + "@rollup/rollup-android-arm64" "4.28.1" + "@rollup/rollup-darwin-arm64" "4.28.1" + "@rollup/rollup-darwin-x64" "4.28.1" + "@rollup/rollup-freebsd-arm64" "4.28.1" + "@rollup/rollup-freebsd-x64" "4.28.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.28.1" + "@rollup/rollup-linux-arm-musleabihf" "4.28.1" + "@rollup/rollup-linux-arm64-gnu" "4.28.1" + "@rollup/rollup-linux-arm64-musl" "4.28.1" + "@rollup/rollup-linux-loongarch64-gnu" "4.28.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.28.1" + "@rollup/rollup-linux-riscv64-gnu" "4.28.1" + "@rollup/rollup-linux-s390x-gnu" "4.28.1" + "@rollup/rollup-linux-x64-gnu" "4.28.1" + "@rollup/rollup-linux-x64-musl" "4.28.1" + "@rollup/rollup-win32-arm64-msvc" "4.28.1" + "@rollup/rollup-win32-ia32-msvc" "4.28.1" + "@rollup/rollup-win32-x64-msvc" "4.28.1" fsevents "~2.3.2" rrweb-cssom@^0.7.1: @@ -7856,9 +7929,9 @@ safe-regex-test@^1.0.3: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.42.1: - version "1.82.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.82.0.tgz#30da277af3d0fa6042e9ceabd0d984ed6d07df70" - integrity sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q== + version "1.83.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.0.tgz#e36842c0b88a94ed336fd16249b878a0541d536f" + integrity sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw== dependencies: chokidar "^4.0.0" immutable "^5.0.2" @@ -8654,9 +8727,9 @@ use-callback-ref@^1.3.0: tslib "^2.0.0" use-sidecar@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" - integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" + integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== dependencies: detect-node-es "^1.1.0" tslib "^2.0.0" @@ -8801,9 +8874,9 @@ vite@^5.0.0: fsevents "~2.3.3" vite@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.2.tgz#7a22630c73c7b663335ddcdb2390971ffbc14993" - integrity sha512-XdQ+VsY2tJpBsKGs0wf3U/+azx8BBpYRHFAyKm5VeEZNOJZRB63q7Sc8Iup3k0TrN3KO6QgyzFf+opSbfY1y0g== + version "6.0.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.3.tgz#cc01f403e326a9fc1e064235df8a6de084c8a491" + integrity sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw== dependencies: esbuild "^0.24.0" postcss "^8.4.49" @@ -9145,6 +9218,6 @@ zod-validation-error@^3.0.3: integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ== zod@^3.22.4: - version "3.24.0" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.0.tgz#babb32313f7c5f4a99812feee806d186b4f76bde" - integrity sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w== + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== From ed269e0711e3e96f12e10bb900dbe24cd6833df8 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:12:12 +0100 Subject: [PATCH 15/19] Revert "Wait for .well-known/matrix/client to load before determining MatrixRTC foci" (#2902) This reverts commit 92afd5d63ae2914f651a3eef93554b3cbf4abb40. --- src/rtcSessionHelper.test.ts | 2 +- src/rtcSessionHelpers.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/rtcSessionHelper.test.ts b/src/rtcSessionHelper.test.ts index 729545f7..7df9f1b3 100644 --- a/src/rtcSessionHelper.test.ts +++ b/src/rtcSessionHelper.test.ts @@ -40,7 +40,7 @@ test("It joins the correct Session", async () => { room: { roomId: "roomId", client: { - waitForClientWellKnown: vi.fn().mockResolvedValue(clientWellKnown), + getClientWellKnown: vi.fn().mockReturnValue(clientWellKnown), }, }, memberships: [], diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index 680df571..f1c7eb8c 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -44,9 +44,8 @@ async function makePreferredLivekitFoci( } // Prioritize the client well known over the configured sfu. - const wellKnownFoci = ( - await rtcSession.room.client.waitForClientWellKnown() - )?.[FOCI_WK_KEY]; + const wellKnownFoci = + rtcSession.room.client.getClientWellKnown()?.[FOCI_WK_KEY]; if (Array.isArray(wellKnownFoci)) { preferredFoci.push( ...wellKnownFoci From 79c40f198cb662481df012594f8fdbbb67be63bd Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 17 Dec 2024 04:01:56 +0000 Subject: [PATCH 16/19] Use finnish notation for observables (#2905) To help make our usage of the observables more readable/intuitive. --- .eslintrc.cjs | 1 + src/analytics/PosthogAnalytics.ts | 2 +- src/controls.ts | 12 +- src/grid/CallLayout.ts | 6 +- src/grid/Grid.tsx | 4 +- src/grid/GridLayout.tsx | 10 +- src/grid/OneOnOneLayout.tsx | 10 +- src/grid/SpotlightExpandedLayout.tsx | 6 +- src/grid/SpotlightLandscapeLayout.tsx | 8 +- src/grid/SpotlightPortraitLayout.tsx | 6 +- src/livekit/MediaDevicesContext.tsx | 10 +- src/room/CallEventAudioRenderer.test.tsx | 28 +- src/room/CallEventAudioRenderer.tsx | 4 +- src/room/InCallView.tsx | 48 +-- src/room/LobbyView.tsx | 2 +- src/room/useSwitchCamera.ts | 24 +- src/settings/settings.ts | 12 +- src/state/CallViewModel.test.ts | 58 ++-- src/state/CallViewModel.ts | 404 ++++++++++++----------- src/state/MediaViewModel.test.ts | 8 +- src/state/MediaViewModel.ts | 108 +++--- src/state/ObservableScope.ts | 12 +- src/state/TileStore.ts | 35 +- src/state/TileViewModel.ts | 6 +- src/state/observeSpeaker.test.ts | 65 ++-- src/state/observeSpeaker.ts | 10 +- src/tile/GridTile.tsx | 24 +- src/tile/SpotlightTile.tsx | 32 +- src/utils/observable.ts | 8 +- src/utils/test.ts | 18 +- 30 files changed, 491 insertions(+), 490 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index db5f3fd9..f338dac7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -44,6 +44,7 @@ module.exports = { ], // To encourage good usage of RxJS: "rxjs/no-exposed-subjects": "error", + "rxjs/finnish": "error", }, settings: { react: { diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 5124e711..2817f78c 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -415,7 +415,7 @@ export class PosthogAnalytics { // * When the user changes their preferences on this device // Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings // won't be called (i.e. this.anonymity will be left as the default, until the setting changes) - optInAnalytics.value.subscribe((optIn) => { + optInAnalytics.value$.subscribe((optIn) => { this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled); this.maybeIdentifyUser().catch(() => logger.log("Could not identify user"), diff --git a/src/controls.ts b/src/controls.ts index 2e8ea7d3..fe8cc4de 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -13,18 +13,18 @@ export interface Controls { disablePip: () => void; } -export const setPipEnabled = new Subject(); +export const setPipEnabled$ = new Subject(); window.controls = { canEnterPip(): boolean { - return setPipEnabled.observed; + return setPipEnabled$.observed; }, enablePip(): void { - if (!setPipEnabled.observed) throw new Error("No call is running"); - setPipEnabled.next(true); + if (!setPipEnabled$.observed) throw new Error("No call is running"); + setPipEnabled$.next(true); }, disablePip(): void { - if (!setPipEnabled.observed) throw new Error("No call is running"); - setPipEnabled.next(false); + if (!setPipEnabled$.observed) throw new Error("No call is running"); + setPipEnabled$.next(false); }, }; diff --git a/src/grid/CallLayout.ts b/src/grid/CallLayout.ts index e05cd025..0e64481a 100644 --- a/src/grid/CallLayout.ts +++ b/src/grid/CallLayout.ts @@ -31,15 +31,15 @@ export interface CallLayoutInputs { /** * The minimum bounds of the layout area. */ - minBounds: Observable; + minBounds$: Observable; /** * The alignment of the floating spotlight tile, if present. */ - spotlightAlignment: BehaviorSubject; + spotlightAlignment$: BehaviorSubject; /** * The alignment of the small picture-in-picture tile, if present. */ - pipAlignment: BehaviorSubject; + pipAlignment$: BehaviorSubject; } export interface CallLayoutOutputs { diff --git a/src/grid/Grid.tsx b/src/grid/Grid.tsx index 268d4352..031a73b5 100644 --- a/src/grid/Grid.tsx +++ b/src/grid/Grid.tsx @@ -156,7 +156,7 @@ export function useVisibleTiles(callback: VisibleTilesCallback): void { ); } -const windowHeightObservable = fromEvent(window, "resize").pipe( +const windowHeightObservable$ = fromEvent(window, "resize").pipe( startWith(null), map(() => window.innerHeight), ); @@ -262,7 +262,7 @@ export function Grid< const [gridRoot, gridRef2] = useState(null); const gridRef = useMergedRefs(gridRef1, gridRef2); - const windowHeight = useObservableEagerState(windowHeightObservable); + const windowHeight = useObservableEagerState(windowHeightObservable$); const [layoutRoot, setLayoutRoot] = useState(null); const [generation, setGeneration] = useState(null); const [visibleTilesCallback, setVisibleTilesCallback] = diff --git a/src/grid/GridLayout.tsx b/src/grid/GridLayout.tsx index f4a29379..fd26c6ee 100644 --- a/src/grid/GridLayout.tsx +++ b/src/grid/GridLayout.tsx @@ -26,8 +26,8 @@ interface GridCSSProperties extends CSSProperties { * together in a scrolling grid. */ export const makeGridLayout: CallLayout = ({ - minBounds, - spotlightAlignment, + minBounds$, + spotlightAlignment$, }) => ({ scrollingOnTop: false, @@ -37,7 +37,7 @@ export const makeGridLayout: CallLayout = ({ useUpdateLayout(); const alignment = useObservableEagerState( useInitial(() => - spotlightAlignment.pipe( + spotlightAlignment$.pipe( distinctUntilChanged( (a1, a2) => a1.block === a2.block && a1.inline === a2.inline, ), @@ -47,7 +47,7 @@ export const makeGridLayout: CallLayout = ({ const onDragSpotlight: DragCallback = useCallback( ({ xRatio, yRatio }) => - spotlightAlignment.next({ + spotlightAlignment$.next({ block: yRatio < 0.5 ? "start" : "end", inline: xRatio < 0.5 ? "start" : "end", }), @@ -74,7 +74,7 @@ export const makeGridLayout: CallLayout = ({ scrolling: forwardRef(function GridLayout({ model, Slot }, ref) { useUpdateLayout(); useVisibleTiles(model.setVisibleTiles); - const { width, height: minHeight } = useObservableEagerState(minBounds); + const { width, height: minHeight } = useObservableEagerState(minBounds$); const { gap, tileWidth, tileHeight } = useMemo( () => arrangeTiles(width, minHeight, model.grid.length), [width, minHeight, model.grid.length], diff --git a/src/grid/OneOnOneLayout.tsx b/src/grid/OneOnOneLayout.tsx index fb0af714..9d49ae90 100644 --- a/src/grid/OneOnOneLayout.tsx +++ b/src/grid/OneOnOneLayout.tsx @@ -19,8 +19,8 @@ import { type DragCallback, useUpdateLayout } from "./Grid"; * is shown at maximum size, overlaid by a small view of the local participant. */ export const makeOneOnOneLayout: CallLayout = ({ - minBounds, - pipAlignment, + minBounds$, + pipAlignment$, }) => ({ scrollingOnTop: false, @@ -31,8 +31,8 @@ export const makeOneOnOneLayout: CallLayout = ({ scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) { useUpdateLayout(); - const { width, height } = useObservableEagerState(minBounds); - const pipAlignmentValue = useObservableEagerState(pipAlignment); + const { width, height } = useObservableEagerState(minBounds$); + const pipAlignmentValue = useObservableEagerState(pipAlignment$); const { tileWidth, tileHeight } = useMemo( () => arrangeTiles(width, height, 1), [width, height], @@ -40,7 +40,7 @@ export const makeOneOnOneLayout: CallLayout = ({ const onDragLocalTile: DragCallback = useCallback( ({ xRatio, yRatio }) => - pipAlignment.next({ + pipAlignment$.next({ block: yRatio < 0.5 ? "start" : "end", inline: xRatio < 0.5 ? "start" : "end", }), diff --git a/src/grid/SpotlightExpandedLayout.tsx b/src/grid/SpotlightExpandedLayout.tsx index d4ce9af3..a50cecb9 100644 --- a/src/grid/SpotlightExpandedLayout.tsx +++ b/src/grid/SpotlightExpandedLayout.tsx @@ -19,7 +19,7 @@ import styles from "./SpotlightExpandedLayout.module.css"; */ export const makeSpotlightExpandedLayout: CallLayout< SpotlightExpandedLayoutModel -> = ({ pipAlignment }) => ({ +> = ({ pipAlignment$ }) => ({ scrollingOnTop: true, fixed: forwardRef(function SpotlightExpandedLayoutFixed( @@ -44,11 +44,11 @@ export const makeSpotlightExpandedLayout: CallLayout< ref, ) { useUpdateLayout(); - const pipAlignmentValue = useObservableEagerState(pipAlignment); + const pipAlignmentValue = useObservableEagerState(pipAlignment$); const onDragPip: DragCallback = useCallback( ({ xRatio, yRatio }) => - pipAlignment.next({ + pipAlignment$.next({ block: yRatio < 0.5 ? "start" : "end", inline: xRatio < 0.5 ? "start" : "end", }), diff --git a/src/grid/SpotlightLandscapeLayout.tsx b/src/grid/SpotlightLandscapeLayout.tsx index 3b80a166..a80cb6fa 100644 --- a/src/grid/SpotlightLandscapeLayout.tsx +++ b/src/grid/SpotlightLandscapeLayout.tsx @@ -21,7 +21,7 @@ import { useUpdateLayout, useVisibleTiles } from "./Grid"; */ export const makeSpotlightLandscapeLayout: CallLayout< SpotlightLandscapeLayoutModel -> = ({ minBounds }) => ({ +> = ({ minBounds$ }) => ({ scrollingOnTop: false, fixed: forwardRef(function SpotlightLandscapeLayoutFixed( @@ -29,7 +29,7 @@ export const makeSpotlightLandscapeLayout: CallLayout< ref, ) { useUpdateLayout(); - useObservableEagerState(minBounds); + useObservableEagerState(minBounds$); return (
@@ -51,9 +51,9 @@ export const makeSpotlightLandscapeLayout: CallLayout< ) { useUpdateLayout(); useVisibleTiles(model.setVisibleTiles); - useObservableEagerState(minBounds); + useObservableEagerState(minBounds$); const withIndicators = - useObservableEagerState(model.spotlight.media).length > 1; + useObservableEagerState(model.spotlight.media$).length > 1; return (
diff --git a/src/grid/SpotlightPortraitLayout.tsx b/src/grid/SpotlightPortraitLayout.tsx index 6af0fa39..9f62a520 100644 --- a/src/grid/SpotlightPortraitLayout.tsx +++ b/src/grid/SpotlightPortraitLayout.tsx @@ -27,7 +27,7 @@ interface GridCSSProperties extends CSSProperties { */ export const makeSpotlightPortraitLayout: CallLayout< SpotlightPortraitLayoutModel -> = ({ minBounds }) => ({ +> = ({ minBounds$ }) => ({ scrollingOnTop: false, fixed: forwardRef(function SpotlightPortraitLayoutFixed( @@ -55,7 +55,7 @@ export const makeSpotlightPortraitLayout: CallLayout< ) { useUpdateLayout(); useVisibleTiles(model.setVisibleTiles); - const { width } = useObservableEagerState(minBounds); + const { width } = useObservableEagerState(minBounds$); const { gap, tileWidth, tileHeight } = arrangeTiles( width, // TODO: We pretend that the minimum height is the width, because the @@ -64,7 +64,7 @@ export const makeSpotlightPortraitLayout: CallLayout< model.grid.length, ); const withIndicators = - useObservableEagerState(model.spotlight.media).length > 1; + useObservableEagerState(model.spotlight.media$).length > 1; return (
createMediaDeviceObserver( kind, @@ -86,7 +86,7 @@ function useMediaDevice( const available = useObservableEagerState( useMemo( () => - deviceObserver.pipe( + deviceObserver$.pipe( map((availableRaw) => { // Sometimes browsers (particularly Firefox) can return multiple device // entries for the exact same device ID; using a map deduplicates them @@ -117,7 +117,7 @@ function useMediaDevice( return available; }), ), - [kind, deviceObserver], + [kind, deviceObserver$], ), ); @@ -140,13 +140,13 @@ function useMediaDevice( const selectedGroupId = useObservableEagerState( useMemo( () => - deviceObserver.pipe( + deviceObserver$.pipe( map( (availableRaw) => availableRaw.find((d) => d.deviceId === selectedId)?.groupId, ), ), - [deviceObserver, selectedId], + [deviceObserver$, selectedId], ), ); diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index e8d22704..6868de49 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -100,13 +100,13 @@ function getMockEnv( ): { vm: CallViewModel; session: MockRTCSession; - remoteRtcMemberships: BehaviorSubject; + remoteRtcMemberships$: BehaviorSubject; } { const matrixRoomMembers = new Map(members.map((p) => [p.userId, p])); - const remoteParticipants = of([aliceParticipant]); + const remoteParticipants$ = of([aliceParticipant]); const liveKitRoom = mockLivekitRoom( { localParticipant }, - { remoteParticipants }, + { remoteParticipants$ }, ); const matrixRoom = mockMatrixRoom({ client: { @@ -118,14 +118,14 @@ function getMockEnv( getMember: (userId) => matrixRoomMembers.get(userId) ?? null, }); - const remoteRtcMemberships = new BehaviorSubject( + const remoteRtcMemberships$ = new BehaviorSubject( initialRemoteRtcMemberships, ); const session = new MockRTCSession( matrixRoom, localRtcMember, - ).withMemberships(remoteRtcMemberships); + ).withMemberships(remoteRtcMemberships$); const vm = new CallViewModel( session as unknown as MatrixRTCSession, @@ -135,7 +135,7 @@ function getMockEnv( }, of(ConnectionState.Connected), ); - return { vm, session, remoteRtcMemberships }; + return { vm, session, remoteRtcMemberships$ }; } /** @@ -146,33 +146,33 @@ function getMockEnv( * a noise every time. */ test("plays one sound when entering a call", () => { - const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]); + const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]); render(); // Joining a call usually means remote participants are added later. act(() => { - remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]); + remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]); }); expect(playSound).toHaveBeenCalledOnce(); }); // TODO: Same test? test("plays a sound when a user joins", () => { - const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]); + const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]); render(); act(() => { - remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]); + remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]); }); // Play a sound when joining a call. expect(playSound).toBeCalledWith("join"); }); test("plays a sound when a user leaves", () => { - const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]); + const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]); render(); act(() => { - remoteRtcMemberships.next([]); + remoteRtcMemberships$.next([]); }); expect(playSound).toBeCalledWith("left"); }); @@ -185,7 +185,7 @@ test("plays no sound when the participant list is more than the maximum size", ( ); } - const { session, vm, remoteRtcMemberships } = getMockEnv( + const { session, vm, remoteRtcMemberships$ } = getMockEnv( [local, alice], mockRtcMemberships, ); @@ -193,7 +193,7 @@ test("plays no sound when the participant list is more than the maximum size", ( render(); expect(playSound).not.toBeCalled(); act(() => { - remoteRtcMemberships.next( + remoteRtcMemberships$.next( mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1), ); }); diff --git a/src/room/CallEventAudioRenderer.tsx b/src/room/CallEventAudioRenderer.tsx index a363c6f5..a2a0a7f1 100644 --- a/src/room/CallEventAudioRenderer.tsx +++ b/src/room/CallEventAudioRenderer.tsx @@ -65,7 +65,7 @@ export function CallEventAudioRenderer({ }, [audioEngineRef, previousRaisedHandCount, raisedHandCount]); useEffect(() => { - const joinSub = vm.memberChanges + const joinSub = vm.memberChanges$ .pipe( filter( ({ joined, ids }) => @@ -77,7 +77,7 @@ export function CallEventAudioRenderer({ void audioEngineRef.current?.playSound("join"); }); - const leftSub = vm.memberChanges + const leftSub = vm.memberChanges$ .pipe( filter( ({ ids, left }) => diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 976e4e94..dde64104 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -110,8 +110,8 @@ export const ActiveCall: FC = (props) => { sfuConfig, props.e2eeSystem, ); - const connStateObservable = useObservable( - (inputs) => inputs.pipe(map(([connState]) => connState)), + const connStateObservable$ = useObservable( + (inputs$) => inputs$.pipe(map(([connState]) => connState)), [connState], ); const [vm, setVm] = useState(null); @@ -131,12 +131,12 @@ export const ActiveCall: FC = (props) => { props.rtcSession, livekitRoom, props.e2eeSystem, - connStateObservable, + connStateObservable$, ); setVm(vm); return (): void => vm.destroy(); } - }, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable]); + }, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable$]); if (livekitRoom === undefined || vm === null) return null; @@ -225,14 +225,14 @@ export const InCallView: FC = ({ () => void toggleRaisedHand(), ); - const windowMode = useObservableEagerState(vm.windowMode); - const layout = useObservableEagerState(vm.layout); - const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration); + const windowMode = useObservableEagerState(vm.windowMode$); + const layout = useObservableEagerState(vm.layout$); + const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration$); const [debugTileLayout] = useSetting(debugTileLayoutSetting); - const gridMode = useObservableEagerState(vm.gridMode); - const showHeader = useObservableEagerState(vm.showHeader); - const showFooter = useObservableEagerState(vm.showFooter); - const switchCamera = useSwitchCamera(vm.localVideo); + const gridMode = useObservableEagerState(vm.gridMode$); + const showHeader = useObservableEagerState(vm.showHeader$); + const showFooter = useObservableEagerState(vm.showFooter$); + const switchCamera = useSwitchCamera(vm.localVideo$); // Ideally we could detect taps by listening for click events and checking // that the pointerType of the event is "touch", but this isn't yet supported @@ -317,15 +317,15 @@ export const InCallView: FC = ({ windowMode, ], ); - const gridBoundsObservable = useObservable( - (inputs) => inputs.pipe(map(([gridBounds]) => gridBounds)), + const gridBoundsObservable$ = useObservable( + (inputs$) => inputs$.pipe(map(([gridBounds]) => gridBounds)), [gridBounds], ); - const spotlightAlignment = useInitial( + const spotlightAlignment$ = useInitial( () => new BehaviorSubject(defaultSpotlightAlignment), ); - const pipAlignment = useInitial( + const pipAlignment$ = useInitial( () => new BehaviorSubject(defaultPipAlignment), ); @@ -383,15 +383,17 @@ export const InCallView: FC = ({ { className, style, targetWidth, targetHeight, model }, ref, ) { - const spotlightExpanded = useObservableEagerState(vm.spotlightExpanded); + const spotlightExpanded = useObservableEagerState( + vm.spotlightExpanded$, + ); const onToggleExpanded = useObservableEagerState( - vm.toggleSpotlightExpanded, + vm.toggleSpotlightExpanded$, ); const showSpeakingIndicatorsValue = useObservableEagerState( - vm.showSpeakingIndicators, + vm.showSpeakingIndicators$, ); const showSpotlightIndicatorsValue = useObservableEagerState( - vm.showSpotlightIndicators, + vm.showSpotlightIndicators$, ); return model instanceof GridTileViewModel ? ( @@ -424,9 +426,9 @@ export const InCallView: FC = ({ const layouts = useMemo(() => { const inputs = { - minBounds: gridBoundsObservable, - spotlightAlignment, - pipAlignment, + minBounds$: gridBoundsObservable$, + spotlightAlignment$, + pipAlignment$, }; return { grid: makeGridLayout(inputs), @@ -435,7 +437,7 @@ export const InCallView: FC = ({ "spotlight-expanded": makeSpotlightExpandedLayout(inputs), "one-on-one": makeOneOnOneLayout(inputs), }; - }, [gridBoundsObservable, spotlightAlignment, pipAlignment]); + }, [gridBoundsObservable$, spotlightAlignment$, pipAlignment$]); const renderContent = (): JSX.Element => { if (layout.type === "pip") { diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index e7dfe3c5..4622cd3e 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -148,7 +148,7 @@ export const LobbyView: FC = ({ const switchCamera = useSwitchCamera( useObservable( - (inputs) => inputs.pipe(map(([video]) => video)), + (inputs$) => inputs$.pipe(map(([video]) => video)), [videoTrack], ), ); diff --git a/src/room/useSwitchCamera.ts b/src/room/useSwitchCamera.ts index 8bbfc92c..1cf5d29d 100644 --- a/src/room/useSwitchCamera.ts +++ b/src/room/useSwitchCamera.ts @@ -31,17 +31,17 @@ import { useLatest } from "../useLatest"; * producing a callback if so. */ export function useSwitchCamera( - video: Observable, + video$: Observable, ): (() => void) | null { const mediaDevices = useMediaDevices(); const setVideoInput = useLatest(mediaDevices.videoInput.select); // Produce an observable like the input 'video' observable, except make it // emit whenever the track is muted or the device changes - const videoTrack: Observable = useObservable( - (inputs) => - inputs.pipe( - switchMap(([video]) => video), + const videoTrack$: Observable = useObservable( + (inputs$) => + inputs$.pipe( + switchMap(([video$]) => video$), switchMap((video) => { if (video === null) return of(null); return merge( @@ -53,15 +53,15 @@ export function useSwitchCamera( ); }), ), - [video], + [video$], ); - const switchCamera: Observable<(() => void) | null> = useObservable( - (inputs) => + const switchCamera$: Observable<(() => void) | null> = useObservable( + (inputs$) => platform === "desktop" ? of(null) - : inputs.pipe( - switchMap(([track]) => track), + : inputs$.pipe( + switchMap(([track$]) => track$), map((track) => { if (track === null) return null; const facingMode = facingModeFromLocalTrack(track).facingMode; @@ -86,8 +86,8 @@ export function useSwitchCamera( ); }), ), - [videoTrack], + [videoTrack$], ); - return useObservableEagerState(switchCamera); + return useObservableEagerState(switchCamera$); } diff --git a/src/settings/settings.ts b/src/settings/settings.ts index a902f9ab..ebb5dffc 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -31,17 +31,17 @@ export class Setting { } } - this._value = new BehaviorSubject(initialValue); - this.value = this._value; + this._value$ = new BehaviorSubject(initialValue); + this.value$ = this._value$; } private readonly key: string; - private readonly _value: BehaviorSubject; - public readonly value: Observable; + private readonly _value$: BehaviorSubject; + public readonly value$: Observable; public readonly setValue = (value: T): void => { - this._value.next(value); + this._value$.next(value); localStorage.setItem(this.key, JSON.stringify(value)); }; } @@ -50,7 +50,7 @@ export class Setting { * React hook that returns a settings's current value and a setter. */ export function useSetting(setting: Setting): [T, (value: T) => void] { - return [useObservableEagerState(setting.value), setting.setValue]; + return [useObservableEagerState(setting.value$), setting.setValue]; } // null = undecided diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel.test.ts index d5b84d49..c9072006 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel.test.ts @@ -124,15 +124,15 @@ export type LayoutSummary = | OneOnOneLayoutSummary | PipLayoutSummary; -function summarizeLayout(l: Observable): Observable { - return l.pipe( +function summarizeLayout$(l$: Observable): Observable { + return l$.pipe( switchMap((l) => { switch (l.type) { case "grid": return combineLatest( [ - l.spotlight?.media ?? of(undefined), - ...l.grid.map((vm) => vm.media), + l.spotlight?.media$ ?? of(undefined), + ...l.grid.map((vm) => vm.media$), ], (spotlight, ...grid) => ({ type: l.type, @@ -143,7 +143,7 @@ function summarizeLayout(l: Observable): Observable { case "spotlight-landscape": case "spotlight-portrait": return combineLatest( - [l.spotlight.media, ...l.grid.map((vm) => vm.media)], + [l.spotlight.media$, ...l.grid.map((vm) => vm.media$)], (spotlight, ...grid) => ({ type: l.type, spotlight: spotlight.map((vm) => vm.id), @@ -152,7 +152,7 @@ function summarizeLayout(l: Observable): Observable { ); case "spotlight-expanded": return combineLatest( - [l.spotlight.media, l.pip?.media ?? of(undefined)], + [l.spotlight.media$, l.pip?.media$ ?? of(undefined)], (spotlight, pip) => ({ type: l.type, spotlight: spotlight.map((vm) => vm.id), @@ -161,7 +161,7 @@ function summarizeLayout(l: Observable): Observable { ); case "one-on-one": return combineLatest( - [l.local.media, l.remote.media], + [l.local.media$, l.remote.media$], (local, remote) => ({ type: l.type, local: local.id, @@ -169,7 +169,7 @@ function summarizeLayout(l: Observable): Observable { }), ); case "pip": - return l.spotlight.media.pipe( + return l.spotlight.media$.pipe( map((spotlight) => ({ type: l.type, spotlight: spotlight.map((vm) => vm.id), @@ -186,9 +186,9 @@ function summarizeLayout(l: Observable): Observable { } function withCallViewModel( - remoteParticipants: Observable, - rtcMembers: Observable[]>, - connectionState: Observable, + remoteParticipants$: Observable, + rtcMembers$: Observable[]>, + connectionState$: Observable, speaking: Map>, continuation: (vm: CallViewModel) => void, ): void { @@ -203,10 +203,10 @@ function withCallViewModel( room, localRtcMember, [], - ).withMemberships(rtcMembers); + ).withMemberships(rtcMembers$); const participantsSpy = vi .spyOn(ComponentsCore, "connectedParticipantsObserver") - .mockReturnValue(remoteParticipants); + .mockReturnValue(remoteParticipants$); const mediaSpy = vi .spyOn(ComponentsCore, "observeParticipantMedia") .mockImplementation((p) => @@ -232,7 +232,7 @@ function withCallViewModel( const liveKitRoom = mockLivekitRoom( { localParticipant }, - { remoteParticipants }, + { remoteParticipants$ }, ); const vm = new CallViewModel( @@ -241,7 +241,7 @@ function withCallViewModel( { kind: E2eeType.PER_PARTICIPANT, }, - connectionState, + connectionState$, ); onTestFinished(() => { @@ -276,7 +276,7 @@ test("participants are retained during a focus switch", () => { }), new Map(), (vm) => { - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { @@ -320,7 +320,7 @@ test("screen sharing activates spotlight layout", () => { g: () => vm.setGridMode("grid"), }); - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { @@ -363,7 +363,7 @@ test("screen sharing activates spotlight layout", () => { }, }, ); - expectObservable(vm.showSpeakingIndicators).toBe( + expectObservable(vm.showSpeakingIndicators$).toBe( expectedShowSpeakingMarbles, { y: true, @@ -402,13 +402,13 @@ test("participants stay in the same order unless to appear/disappear", () => { a: () => { // We imagine that only three tiles (the first three) will be visible // on screen at a time - vm.layout.subscribe((layout) => { + vm.layout$.subscribe((layout) => { if (layout.type === "grid") layout.setVisibleTiles(3); }); }, }); - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { @@ -455,7 +455,7 @@ test("participants adjust order when space becomes constrained", () => { ]), (vm) => { let setVisibleTiles: ((value: number) => void) | null = null; - vm.layout.subscribe((layout) => { + vm.layout$.subscribe((layout) => { if (layout.type === "grid") setVisibleTiles = layout.setVisibleTiles; }); schedule(visibilityInputMarbles, { @@ -463,7 +463,7 @@ test("participants adjust order when space becomes constrained", () => { b: () => setVisibleTiles!(3), }); - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { @@ -509,7 +509,7 @@ test("spotlight speakers swap places", () => { (vm) => { schedule(modeInputMarbles, { s: () => vm.setGridMode("spotlight") }); - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { @@ -557,7 +557,7 @@ test("layout enters picture-in-picture mode when requested", () => { d: () => window.controls.disablePip(), }); - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { @@ -600,12 +600,12 @@ test("spotlight remembers whether it's expanded", () => { schedule(expandInputMarbles, { a: () => { let toggle: () => void; - vm.toggleSpotlightExpanded.subscribe((val) => (toggle = val!)); + vm.toggleSpotlightExpanded$.subscribe((val) => (toggle = val!)); toggle!(); }, }); - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { @@ -662,7 +662,7 @@ test("participants must have a MatrixRTCSession to be visible", () => { new Map(), (vm) => { vm.setGridMode("grid"); - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { @@ -706,7 +706,7 @@ test("shows participants without MatrixRTCSession when enabled in settings", () new Map(), (vm) => { vm.setGridMode("grid"); - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { @@ -753,7 +753,7 @@ it("should show at least one tile per MatrixRTCSession", () => { new Map(), (vm) => { vm.setGridMode("grid"); - expectObservable(summarizeLayout(vm.layout)).toBe( + expectObservable(summarizeLayout$(vm.layout$)).toBe( expectedLayoutMarbles, { a: { diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 8e080fdc..36cbbac8 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -62,7 +62,7 @@ import { import { LocalUserMediaViewModel, type MediaViewModel, - observeTrackReference, + observeTrackReference$, RemoteUserMediaViewModel, ScreenShareViewModel, type UserMediaViewModel, @@ -71,7 +71,7 @@ import { accumulate, finalizeValue } from "../utils/observable"; import { ObservableScope } from "./ObservableScope"; import { duplicateTiles, showNonMemberTiles } from "../settings/settings"; import { isFirefox } from "../Platform"; -import { setPipEnabled } from "../controls"; +import { setPipEnabled$ } from "../controls"; import { type GridTileViewModel, type SpotlightTileViewModel, @@ -82,7 +82,7 @@ import { spotlightExpandedLayout } from "./SpotlightExpandedLayout"; import { oneOnOneLayout } from "./OneOnOneLayout"; import { pipLayout } from "./PipLayout"; import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; -import { observeSpeaker } from "./observeSpeaker"; +import { observeSpeaker$ } from "./observeSpeaker"; import { shallowEquals } from "../utils/array"; // How long we wait after a focus switch before showing the real participant @@ -232,12 +232,12 @@ interface LayoutScanState { class UserMedia { private readonly scope = new ObservableScope(); public readonly vm: UserMediaViewModel; - private readonly participant: BehaviorSubject< + private readonly participant$: BehaviorSubject< LocalParticipant | RemoteParticipant | undefined >; - public readonly speaker: Observable; - public readonly presenter: Observable; + public readonly speaker$: Observable; + public readonly presenter$: Observable; public constructor( public readonly id: string, member: RoomMember | undefined, @@ -245,13 +245,13 @@ class UserMedia { encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, ) { - this.participant = new BehaviorSubject(participant); + this.participant$ = new BehaviorSubject(participant); if (participant?.isLocal) { this.vm = new LocalUserMediaViewModel( this.id, member, - this.participant.asObservable() as Observable, + this.participant$.asObservable() as Observable, encryptionSystem, livekitRoom, ); @@ -259,7 +259,7 @@ class UserMedia { this.vm = new RemoteUserMediaViewModel( id, member, - this.participant.asObservable() as Observable< + this.participant$.asObservable() as Observable< RemoteParticipant | undefined >, encryptionSystem, @@ -267,9 +267,9 @@ class UserMedia { ); } - this.speaker = observeSpeaker(this.vm.speaking).pipe(this.scope.state()); + this.speaker$ = observeSpeaker$(this.vm.speaking$).pipe(this.scope.state()); - this.presenter = this.participant.pipe( + this.presenter$ = this.participant$.pipe( switchMap( (p) => (p && @@ -289,9 +289,9 @@ class UserMedia { public updateParticipant( newParticipant: LocalParticipant | RemoteParticipant | undefined, ): void { - if (this.participant.value !== newParticipant) { + if (this.participant$.value !== newParticipant) { // Update the BehaviourSubject in the UserMedia. - this.participant.next(newParticipant); + this.participant$.next(newParticipant); } } @@ -303,7 +303,7 @@ class UserMedia { class ScreenShare { public readonly vm: ScreenShareViewModel; - private readonly participant: BehaviorSubject< + private readonly participant$: BehaviorSubject< LocalParticipant | RemoteParticipant >; @@ -314,12 +314,12 @@ class ScreenShare { encryptionSystem: EncryptionSystem, liveKitRoom: LivekitRoom, ) { - this.participant = new BehaviorSubject(participant); + this.participant$ = new BehaviorSubject(participant); this.vm = new ScreenShareViewModel( id, member, - this.participant.asObservable(), + this.participant$.asObservable(), encryptionSystem, liveKitRoom, participant.isLocal, @@ -357,8 +357,8 @@ function findMatrixRoomMember( // TODO: Move wayyyy more business logic from the call and lobby views into here export class CallViewModel extends ViewModel { - public readonly localVideo: Observable = - observeTrackReference( + public readonly localVideo$: Observable = + observeTrackReference$( of(this.livekitRoom.localParticipant), Track.Source.Camera, ).pipe( @@ -371,16 +371,16 @@ export class CallViewModel extends ViewModel { /** * The raw list of RemoteParticipants as reported by LiveKit */ - private readonly rawRemoteParticipants: Observable = + private readonly rawRemoteParticipants$: Observable = connectedParticipantsObserver(this.livekitRoom).pipe(this.scope.state()); /** * Lists of RemoteParticipants to "hold" on display, even if LiveKit claims that * they've left */ - private readonly remoteParticipantHolds: Observable = - this.connectionState.pipe( - withLatestFrom(this.rawRemoteParticipants), + private readonly remoteParticipantHolds$: Observable = + this.connectionState$.pipe( + withLatestFrom(this.rawRemoteParticipants$), mergeMap(([s, ps]) => { // Whenever we switch focuses, we should retain all the previous // participants for at least POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS ms to @@ -392,7 +392,7 @@ export class CallViewModel extends ViewModel { // Wait for time to pass and the connection state to have changed forkJoin([ timer(POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS), - this.connectionState.pipe( + this.connectionState$.pipe( filter((s) => s !== ECAddonConnectionState.ECSwitchingFocus), take(1), ), @@ -415,9 +415,9 @@ export class CallViewModel extends ViewModel { /** * The RemoteParticipants including those that are being "held" on the screen */ - private readonly remoteParticipants: Observable = + private readonly remoteParticipants$: Observable = combineLatest( - [this.rawRemoteParticipants, this.remoteParticipantHolds], + [this.rawRemoteParticipants$, this.remoteParticipantHolds$], (raw, holds) => { const result = [...raw]; const resultIds = new Set(result.map((p) => p.identity)); @@ -439,10 +439,10 @@ export class CallViewModel extends ViewModel { /** * List of MediaItems that we want to display */ - private readonly mediaItems: Observable = combineLatest([ - this.remoteParticipants, + private readonly mediaItems$: Observable = combineLatest([ + this.remoteParticipants$, observeParticipantMedia(this.livekitRoom.localParticipant), - duplicateTiles.value, + duplicateTiles.value$, // Also react to changes in the MatrixRTC session list. // The session list will also be update if a room membership changes. // No additional RoomState event listener needs to be set up. @@ -450,7 +450,7 @@ export class CallViewModel extends ViewModel { this.matrixRTCSession, MatrixRTCSessionEvent.MembershipsChanged, ).pipe(startWith(null)), - showNonMemberTiles.value, + showNonMemberTiles.value$, ]).pipe( scan( ( @@ -606,13 +606,13 @@ export class CallViewModel extends ViewModel { /** * List of MediaItems that we want to display, that are of type UserMedia */ - private readonly userMedia: Observable = this.mediaItems.pipe( + private readonly userMedia$: Observable = this.mediaItems$.pipe( map((mediaItems) => mediaItems.filter((m): m is UserMedia => m instanceof UserMedia), ), ); - public readonly memberChanges = this.userMedia + public readonly memberChanges$ = this.userMedia$ .pipe(map((mediaItems) => mediaItems.map((m) => m.id))) .pipe( scan( @@ -628,22 +628,22 @@ export class CallViewModel extends ViewModel { /** * List of MediaItems that we want to display, that are of type ScreenShare */ - private readonly screenShares: Observable = - this.mediaItems.pipe( + private readonly screenShares$: Observable = + this.mediaItems$.pipe( map((mediaItems) => mediaItems.filter((m): m is ScreenShare => m instanceof ScreenShare), ), this.scope.state(), ); - private readonly spotlightSpeaker: Observable = - this.userMedia.pipe( + private readonly spotlightSpeaker$: Observable = + this.userMedia$.pipe( switchMap((mediaItems) => mediaItems.length === 0 ? of([]) : combineLatest( mediaItems.map((m) => - m.vm.speaking.pipe(map((s) => [m, s] as const)), + m.vm.speaking$.pipe(map((s) => [m, s] as const)), ), ), ), @@ -672,52 +672,53 @@ export class CallViewModel extends ViewModel { this.scope.state(), ); - private readonly grid: Observable = this.userMedia.pipe( - switchMap((mediaItems) => { - const bins = mediaItems.map((m) => - combineLatest( - [ - m.speaker, - m.presenter, - m.vm.videoEnabled, - m.vm instanceof LocalUserMediaViewModel - ? m.vm.alwaysShow - : of(false), - ], - (speaker, presenter, video, alwaysShow) => { - let bin: SortingBin; - if (m.vm.local) - bin = alwaysShow - ? SortingBin.SelfAlwaysShown - : SortingBin.SelfNotAlwaysShown; - else if (presenter) bin = SortingBin.Presenters; - else if (speaker) bin = SortingBin.Speakers; - else if (video) bin = SortingBin.Video; - else bin = SortingBin.NoVideo; + private readonly grid$: Observable = + this.userMedia$.pipe( + switchMap((mediaItems) => { + const bins = mediaItems.map((m) => + combineLatest( + [ + m.speaker$, + m.presenter$, + m.vm.videoEnabled$, + m.vm instanceof LocalUserMediaViewModel + ? m.vm.alwaysShow$ + : of(false), + ], + (speaker, presenter, video, alwaysShow) => { + let bin: SortingBin; + if (m.vm.local) + bin = alwaysShow + ? SortingBin.SelfAlwaysShown + : SortingBin.SelfNotAlwaysShown; + else if (presenter) bin = SortingBin.Presenters; + else if (speaker) bin = SortingBin.Speakers; + else if (video) bin = SortingBin.Video; + else bin = SortingBin.NoVideo; - return [m, bin] as const; - }, - ), - ); - // Sort the media by bin order and generate a tile for each one - return bins.length === 0 - ? of([]) - : combineLatest(bins, (...bins) => - bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm), - ); - }), - distinctUntilChanged(shallowEquals), - this.scope.state(), - ); + return [m, bin] as const; + }, + ), + ); + // Sort the media by bin order and generate a tile for each one + return bins.length === 0 + ? of([]) + : combineLatest(bins, (...bins) => + bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm), + ); + }), + distinctUntilChanged(shallowEquals), + this.scope.state(), + ); - private readonly spotlight: Observable = - this.screenShares.pipe( + private readonly spotlight$: Observable = + this.screenShares$.pipe( switchMap((screenShares) => { if (screenShares.length > 0) { return of(screenShares.map((m) => m.vm)); } - return this.spotlightSpeaker.pipe( + return this.spotlightSpeaker$.pipe( map((speaker) => (speaker ? [speaker] : [])), ); }), @@ -725,14 +726,14 @@ export class CallViewModel extends ViewModel { this.scope.state(), ); - private readonly pip: Observable = combineLatest([ - this.screenShares, - this.spotlightSpeaker, - this.mediaItems, + private readonly pip$: Observable = combineLatest([ + this.screenShares$, + this.spotlightSpeaker$, + this.mediaItems$, ]).pipe( switchMap(([screenShares, spotlight, mediaItems]) => { if (screenShares.length > 0) { - return this.spotlightSpeaker; + return this.spotlightSpeaker$; } if (!spotlight || spotlight.local) { return of(null); @@ -749,7 +750,7 @@ export class CallViewModel extends ViewModel { if (!localUserMediaViewModel) { return of(null); } - return localUserMediaViewModel.alwaysShow.pipe( + return localUserMediaViewModel.alwaysShow$.pipe( map((alwaysShow) => { if (alwaysShow) { return localUserMediaViewModel; @@ -762,19 +763,19 @@ export class CallViewModel extends ViewModel { this.scope.state(), ); - private readonly hasRemoteScreenShares: Observable = - this.spotlight.pipe( + private readonly hasRemoteScreenShares$: Observable = + this.spotlight$.pipe( map((spotlight) => spotlight.some((vm) => !vm.local && vm instanceof ScreenShareViewModel), ), distinctUntilChanged(), ); - private readonly pipEnabled: Observable = setPipEnabled.pipe( + private readonly pipEnabled$: Observable = setPipEnabled$.pipe( startWith(false), ); - private readonly naturalWindowMode: Observable = fromEvent( + private readonly naturalWindowMode$: Observable = fromEvent( window, "resize", ).pipe( @@ -796,30 +797,30 @@ export class CallViewModel extends ViewModel { /** * The general shape of the window. */ - public readonly windowMode: Observable = this.pipEnabled.pipe( - switchMap((pip) => (pip ? of("pip") : this.naturalWindowMode)), + public readonly windowMode$: Observable = this.pipEnabled$.pipe( + switchMap((pip) => (pip ? of("pip") : this.naturalWindowMode$)), ); - private readonly spotlightExpandedToggle = new Subject(); - public readonly spotlightExpanded: Observable = - this.spotlightExpandedToggle.pipe( + private readonly spotlightExpandedToggle$ = new Subject(); + public readonly spotlightExpanded$: Observable = + this.spotlightExpandedToggle$.pipe( accumulate(false, (expanded) => !expanded), this.scope.state(), ); - private readonly gridModeUserSelection = new Subject(); + private readonly gridModeUserSelection$ = new Subject(); /** * The layout mode of the media tile grid. */ - public readonly gridMode: Observable = + public readonly gridMode$: Observable = // If the user hasn't selected spotlight and somebody starts screen sharing, // automatically switch to spotlight mode and reset when screen sharing ends - this.gridModeUserSelection.pipe( + this.gridModeUserSelection$.pipe( startWith(null), switchMap((userSelection) => (userSelection === "spotlight" ? EMPTY - : combineLatest([this.hasRemoteScreenShares, this.windowMode]).pipe( + : combineLatest([this.hasRemoteScreenShares$, this.windowMode$]).pipe( skip(userSelection === null ? 0 : 1), map( ([hasScreenShares, windowMode]): GridMode => @@ -834,43 +835,41 @@ export class CallViewModel extends ViewModel { ); public setGridMode(value: GridMode): void { - this.gridModeUserSelection.next(value); + this.gridModeUserSelection$.next(value); } - private readonly gridLayoutMedia: Observable = combineLatest( - [this.grid, this.spotlight], - (grid, spotlight) => ({ + private readonly gridLayoutMedia$: Observable = + combineLatest([this.grid$, this.spotlight$], (grid, spotlight) => ({ type: "grid", spotlight: spotlight.some((vm) => vm instanceof ScreenShareViewModel) ? spotlight : undefined, grid, - }), - ); + })); - private readonly spotlightLandscapeLayoutMedia: Observable = - combineLatest([this.grid, this.spotlight], (grid, spotlight) => ({ + private readonly spotlightLandscapeLayoutMedia$: Observable = + combineLatest([this.grid$, this.spotlight$], (grid, spotlight) => ({ type: "spotlight-landscape", spotlight, grid, })); - private readonly spotlightPortraitLayoutMedia: Observable = - combineLatest([this.grid, this.spotlight], (grid, spotlight) => ({ + private readonly spotlightPortraitLayoutMedia$: Observable = + combineLatest([this.grid$, this.spotlight$], (grid, spotlight) => ({ type: "spotlight-portrait", spotlight, grid, })); - private readonly spotlightExpandedLayoutMedia: Observable = - combineLatest([this.spotlight, this.pip], (spotlight, pip) => ({ + private readonly spotlightExpandedLayoutMedia$: Observable = + combineLatest([this.spotlight$, this.pip$], (spotlight, pip) => ({ type: "spotlight-expanded", spotlight, pip: pip ?? undefined, })); - private readonly oneOnOneLayoutMedia: Observable = - this.mediaItems.pipe( + private readonly oneOnOneLayoutMedia$: Observable = + this.mediaItems$.pipe( map((mediaItems) => { if (mediaItems.length !== 2) return null; const local = mediaItems.find((vm) => vm.vm.local)?.vm as @@ -888,86 +887,91 @@ export class CallViewModel extends ViewModel { }), ); - private readonly pipLayoutMedia: Observable = - this.spotlight.pipe(map((spotlight) => ({ type: "pip", spotlight }))); + private readonly pipLayoutMedia$: Observable = + this.spotlight$.pipe(map((spotlight) => ({ type: "pip", spotlight }))); /** * The media to be used to produce a layout. */ - private readonly layoutMedia: Observable = this.windowMode.pipe( - switchMap((windowMode) => { - switch (windowMode) { - case "normal": - return this.gridMode.pipe( - switchMap((gridMode) => { - switch (gridMode) { - case "grid": - return this.oneOnOneLayoutMedia.pipe( - switchMap((oneOnOne) => - oneOnOne === null ? this.gridLayoutMedia : of(oneOnOne), - ), - ); - case "spotlight": - return this.spotlightExpanded.pipe( - switchMap((expanded) => - expanded - ? this.spotlightExpandedLayoutMedia - : this.spotlightLandscapeLayoutMedia, - ), - ); - } - }), - ); - case "narrow": - return this.oneOnOneLayoutMedia.pipe( - switchMap((oneOnOne) => - oneOnOne === null - ? combineLatest( - [this.grid, this.spotlight], - (grid, spotlight) => - grid.length > smallMobileCallThreshold || - spotlight.some((vm) => vm instanceof ScreenShareViewModel) - ? this.spotlightPortraitLayoutMedia - : this.gridLayoutMedia, - ).pipe(switchAll()) - : // The expanded spotlight layout makes for a better one-on-one - // experience in narrow windows - this.spotlightExpandedLayoutMedia, - ), - ); - case "flat": - return this.gridMode.pipe( - switchMap((gridMode) => { - switch (gridMode) { - case "grid": - // Yes, grid mode actually gets you a "spotlight" layout in - // this window mode. - return this.spotlightLandscapeLayoutMedia; - case "spotlight": - return this.spotlightExpandedLayoutMedia; - } - }), - ); - case "pip": - return this.pipLayoutMedia; - } - }), - this.scope.state(), - ); + private readonly layoutMedia$: Observable = + this.windowMode$.pipe( + switchMap((windowMode) => { + switch (windowMode) { + case "normal": + return this.gridMode$.pipe( + switchMap((gridMode) => { + switch (gridMode) { + case "grid": + return this.oneOnOneLayoutMedia$.pipe( + switchMap((oneOnOne) => + oneOnOne === null + ? this.gridLayoutMedia$ + : of(oneOnOne), + ), + ); + case "spotlight": + return this.spotlightExpanded$.pipe( + switchMap((expanded) => + expanded + ? this.spotlightExpandedLayoutMedia$ + : this.spotlightLandscapeLayoutMedia$, + ), + ); + } + }), + ); + case "narrow": + return this.oneOnOneLayoutMedia$.pipe( + switchMap((oneOnOne) => + oneOnOne === null + ? combineLatest( + [this.grid$, this.spotlight$], + (grid, spotlight) => + grid.length > smallMobileCallThreshold || + spotlight.some( + (vm) => vm instanceof ScreenShareViewModel, + ) + ? this.spotlightPortraitLayoutMedia$ + : this.gridLayoutMedia$, + ).pipe(switchAll()) + : // The expanded spotlight layout makes for a better one-on-one + // experience in narrow windows + this.spotlightExpandedLayoutMedia$, + ), + ); + case "flat": + return this.gridMode$.pipe( + switchMap((gridMode) => { + switch (gridMode) { + case "grid": + // Yes, grid mode actually gets you a "spotlight" layout in + // this window mode. + return this.spotlightLandscapeLayoutMedia$; + case "spotlight": + return this.spotlightExpandedLayoutMedia$; + } + }), + ); + case "pip": + return this.pipLayoutMedia$; + } + }), + this.scope.state(), + ); // There is a cyclical dependency here: the layout algorithms want to know // which tiles are on screen, but to know which tiles are on screen we have to // first render a layout. To deal with this we assume initially that no tiles // are visible, and loop the data back into the layouts with a Subject. - private readonly visibleTiles = new Subject(); + private readonly visibleTiles$ = new Subject(); private readonly setVisibleTiles = (value: number): void => - this.visibleTiles.next(value); + this.visibleTiles$.next(value); - public readonly layoutInternals: Observable< + public readonly layoutInternals$: Observable< LayoutScanState & { layout: Layout } > = combineLatest([ - this.layoutMedia, - this.visibleTiles.pipe(startWith(0), distinctUntilChanged()), + this.layoutMedia$, + this.visibleTiles$.pipe(startWith(0), distinctUntilChanged()), ]).pipe( scan< [LayoutMedia, number], @@ -1009,7 +1013,7 @@ export class CallViewModel extends ViewModel { /** * The layout of tiles in the call interface. */ - public readonly layout: Observable = this.layoutInternals.pipe( + public readonly layout$: Observable = this.layoutInternals$.pipe( map(({ layout }) => layout), this.scope.state(), ); @@ -1017,18 +1021,18 @@ export class CallViewModel extends ViewModel { /** * The current generation of the tile store, exposed for debugging purposes. */ - public readonly tileStoreGeneration: Observable = - this.layoutInternals.pipe( + public readonly tileStoreGeneration$: Observable = + this.layoutInternals$.pipe( map(({ tiles }) => tiles.generation), this.scope.state(), ); - public showSpotlightIndicators: Observable = this.layout.pipe( + public showSpotlightIndicators$: Observable = this.layout$.pipe( map((l) => l.type !== "grid"), this.scope.state(), ); - public showSpeakingIndicators: Observable = this.layout.pipe( + public showSpeakingIndicators$: Observable = this.layout$.pipe( switchMap((l) => { switch (l.type) { case "spotlight-landscape": @@ -1036,7 +1040,7 @@ export class CallViewModel extends ViewModel { // If the spotlight is showing the active speaker, we can do without // speaking indicators as they're a redundant visual cue. But if // screen sharing feeds are in the spotlight we still need them. - return l.spotlight.media.pipe( + return l.spotlight.media$.pipe( map((models: MediaViewModel[]) => models.some((m) => m instanceof ScreenShareViewModel), ), @@ -1055,11 +1059,11 @@ export class CallViewModel extends ViewModel { this.scope.state(), ); - public readonly toggleSpotlightExpanded: Observable<(() => void) | null> = - this.windowMode.pipe( + public readonly toggleSpotlightExpanded$: Observable<(() => void) | null> = + this.windowMode$.pipe( switchMap((mode) => mode === "normal" - ? this.layout.pipe( + ? this.layout$.pipe( map( (l) => l.type === "spotlight-landscape" || @@ -1070,50 +1074,50 @@ export class CallViewModel extends ViewModel { ), distinctUntilChanged(), map((enabled) => - enabled ? (): void => this.spotlightExpandedToggle.next() : null, + enabled ? (): void => this.spotlightExpandedToggle$.next() : null, ), this.scope.state(), ); - private readonly screenTap = new Subject(); - private readonly controlsTap = new Subject(); - private readonly screenHover = new Subject(); - private readonly screenUnhover = new Subject(); + private readonly screenTap$ = new Subject(); + private readonly controlsTap$ = new Subject(); + private readonly screenHover$ = new Subject(); + private readonly screenUnhover$ = new Subject(); /** * Callback for when the user taps the call view. */ public tapScreen(): void { - this.screenTap.next(); + this.screenTap$.next(); } /** * Callback for when the user taps the call's controls. */ public tapControls(): void { - this.controlsTap.next(); + this.controlsTap$.next(); } /** * Callback for when the user hovers over the call view. */ public hoverScreen(): void { - this.screenHover.next(); + this.screenHover$.next(); } /** * Callback for when the user stops hovering over the call view. */ public unhoverScreen(): void { - this.screenUnhover.next(); + this.screenUnhover$.next(); } - public readonly showHeader: Observable = this.windowMode.pipe( + public readonly showHeader$: Observable = this.windowMode$.pipe( map((mode) => mode !== "pip" && mode !== "flat"), this.scope.state(), ); - public readonly showFooter: Observable = this.windowMode.pipe( + public readonly showFooter$: Observable = this.windowMode$.pipe( switchMap((mode) => { switch (mode) { case "pip": @@ -1128,9 +1132,9 @@ export class CallViewModel extends ViewModel { if (isFirefox()) return of(true); // Show/hide the footer in response to interactions return merge( - this.screenTap.pipe(map(() => "tap screen" as const)), - this.controlsTap.pipe(map(() => "tap controls" as const)), - this.screenHover.pipe(map(() => "hover" as const)), + this.screenTap$.pipe(map(() => "tap screen" as const)), + this.controlsTap$.pipe(map(() => "tap controls" as const)), + this.screenHover$.pipe(map(() => "hover" as const)), ).pipe( switchScan((state, interaction) => { switch (interaction) { @@ -1153,7 +1157,7 @@ export class CallViewModel extends ViewModel { // Show on hover and hide after a timeout return race( timer(showFooterMs), - this.screenUnhover.pipe(take(1)), + this.screenUnhover$.pipe(take(1)), ).pipe( map(() => false), startWith(true), @@ -1172,7 +1176,7 @@ export class CallViewModel extends ViewModel { private readonly matrixRTCSession: MatrixRTCSession, private readonly livekitRoom: LivekitRoom, private readonly encryptionSystem: EncryptionSystem, - private readonly connectionState: Observable, + private readonly connectionState$: Observable, ) { super(); } diff --git a/src/state/MediaViewModel.test.ts b/src/state/MediaViewModel.test.ts index c4e0bee6..18fa13b6 100644 --- a/src/state/MediaViewModel.test.ts +++ b/src/state/MediaViewModel.test.ts @@ -49,7 +49,7 @@ test("control a participant's volume", async () => { expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8); }, }); - expectObservable(vm.localVolume).toBe("ab(cd)(ef)g", { + expectObservable(vm.localVolume$).toBe("ab(cd)(ef)g", { a: 1, b: 0, c: 0.6, @@ -69,7 +69,7 @@ test("toggle fit/contain for a participant's video", async () => { a: () => vm.toggleFitContain(), b: () => vm.toggleFitContain(), }); - expectObservable(vm.cropVideo).toBe("abc", { + expectObservable(vm.cropVideo$).toBe("abc", { a: true, b: false, c: true, @@ -82,7 +82,7 @@ test("local media remembers whether it should always be shown", async () => { await withLocalMedia(rtcMembership, {}, (vm) => withTestScheduler(({ expectObservable, schedule }) => { schedule("-a|", { a: () => vm.setAlwaysShow(false) }); - expectObservable(vm.alwaysShow).toBe("ab", { a: true, b: false }); + expectObservable(vm.alwaysShow$).toBe("ab", { a: true, b: false }); }), ); // Next local media should start out *not* always shown @@ -93,7 +93,7 @@ test("local media remembers whether it should always be shown", async () => { (vm) => withTestScheduler(({ expectObservable, schedule }) => { schedule("-a|", { a: () => vm.setAlwaysShow(true) }); - expectObservable(vm.alwaysShow).toBe("ab", { a: false, b: true }); + expectObservable(vm.alwaysShow$).toBe("ab", { a: false, b: true }); }), ); }); diff --git a/src/state/MediaViewModel.ts b/src/state/MediaViewModel.ts index ea015eb8..8100a50d 100644 --- a/src/state/MediaViewModel.ts +++ b/src/state/MediaViewModel.ts @@ -74,11 +74,11 @@ export function useDisplayName(vm: MediaViewModel): string { return displayName; } -export function observeTrackReference( - participant: Observable, +export function observeTrackReference$( + participant$: Observable, source: Track.Source, ): Observable { - return participant.pipe( + return participant$.pipe( switchMap((p) => { if (p) { return observeParticipantMedia(p).pipe( @@ -96,7 +96,7 @@ export function observeTrackReference( ); } -function observeRemoteTrackReceivingOkay( +function observeRemoteTrackReceivingOkay$( participant: Participant, source: Track.Source, ): Observable { @@ -111,7 +111,7 @@ function observeRemoteTrackReceivingOkay( }; return combineLatest([ - observeTrackReference(of(participant), source), + observeTrackReference$(of(participant), source), interval(1000).pipe(startWith(0)), ]).pipe( switchMap(async ([trackReference]) => { @@ -168,7 +168,7 @@ function observeRemoteTrackReceivingOkay( ); } -function encryptionErrorObservable( +function encryptionErrorObservable$( room: LivekitRoom, participant: Participant, encryptionSystem: EncryptionSystem, @@ -209,13 +209,13 @@ abstract class BaseMediaViewModel extends ViewModel { /** * The LiveKit video track for this media. */ - public readonly video: Observable; + public readonly video$: Observable; /** * Whether there should be a warning that this media is unencrypted. */ - public readonly unencryptedWarning: Observable; + public readonly unencryptedWarning$: Observable; - public readonly encryptionStatus: Observable; + public readonly encryptionStatus$: Observable; /** * Whether this media corresponds to the local participant. @@ -235,7 +235,7 @@ abstract class BaseMediaViewModel extends ViewModel { public readonly member: RoomMember | undefined, // We don't necessarily have a participant if a user connects via MatrixRTC but not (yet) through // livekit. - protected readonly participant: Observable< + protected readonly participant$: Observable< LocalParticipant | RemoteParticipant | undefined >, @@ -245,21 +245,21 @@ abstract class BaseMediaViewModel extends ViewModel { livekitRoom: LivekitRoom, ) { super(); - const audio = observeTrackReference(participant, audioSource).pipe( + const audio$ = observeTrackReference$(participant$, audioSource).pipe( this.scope.state(), ); - this.video = observeTrackReference(participant, videoSource).pipe( + this.video$ = observeTrackReference$(participant$, videoSource).pipe( this.scope.state(), ); - this.unencryptedWarning = combineLatest( - [audio, this.video], + this.unencryptedWarning$ = combineLatest( + [audio$, this.video$], (a, v) => encryptionSystem.kind !== E2eeType.NONE && (a?.publication?.isEncrypted === false || v?.publication?.isEncrypted === false), ).pipe(this.scope.state()); - this.encryptionStatus = this.participant.pipe( + this.encryptionStatus$ = this.participant$.pipe( switchMap((participant): Observable => { if (!participant) { return of(EncryptionStatus.Connecting); @@ -270,20 +270,20 @@ abstract class BaseMediaViewModel extends ViewModel { return of(EncryptionStatus.Okay); } else if (encryptionSystem.kind === E2eeType.PER_PARTICIPANT) { return combineLatest([ - encryptionErrorObservable( + encryptionErrorObservable$( livekitRoom, participant, encryptionSystem, "MissingKey", ), - encryptionErrorObservable( + encryptionErrorObservable$( livekitRoom, participant, encryptionSystem, "InvalidKey", ), - observeRemoteTrackReceivingOkay(participant, audioSource), - observeRemoteTrackReceivingOkay(participant, videoSource), + observeRemoteTrackReceivingOkay$(participant, audioSource), + observeRemoteTrackReceivingOkay$(participant, videoSource), ]).pipe( map(([keyMissing, keyInvalid, audioOkay, videoOkay]) => { if (keyMissing) return EncryptionStatus.KeyMissing; @@ -296,14 +296,14 @@ abstract class BaseMediaViewModel extends ViewModel { ); } else { return combineLatest([ - encryptionErrorObservable( + encryptionErrorObservable$( livekitRoom, participant, encryptionSystem, "InvalidKey", ), - observeRemoteTrackReceivingOkay(participant, audioSource), - observeRemoteTrackReceivingOkay(participant, videoSource), + observeRemoteTrackReceivingOkay$(participant, audioSource), + observeRemoteTrackReceivingOkay$(participant, videoSource), ]).pipe( map( ([keyInvalid, audioOkay, videoOkay]): @@ -339,7 +339,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { /** * Whether the participant is speaking. */ - public readonly speaking = this.participant.pipe( + public readonly speaking$ = this.participant$.pipe( switchMap((p) => p ? observeParticipantEvents(p, ParticipantEvent.IsSpeakingChanged).pipe( @@ -353,49 +353,49 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { /** * Whether this participant is sending audio (i.e. is unmuted on their side). */ - public readonly audioEnabled: Observable; + public readonly audioEnabled$: Observable; /** * Whether this participant is sending video. */ - public readonly videoEnabled: Observable; + public readonly videoEnabled$: Observable; - private readonly _cropVideo = new BehaviorSubject(true); + private readonly _cropVideo$ = new BehaviorSubject(true); /** * Whether the tile video should be contained inside the tile or be cropped to fit. */ - public readonly cropVideo: Observable = this._cropVideo; + public readonly cropVideo$: Observable = this._cropVideo$; public constructor( id: string, member: RoomMember | undefined, - participant: Observable, + participant$: Observable, encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, ) { super( id, member, - participant, + participant$, encryptionSystem, Track.Source.Microphone, Track.Source.Camera, livekitRoom, ); - const media = participant.pipe( + const media$ = participant$.pipe( switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)), this.scope.state(), ); - this.audioEnabled = media.pipe( + this.audioEnabled$ = media$.pipe( map((m) => m?.microphoneTrack?.isMuted === false), ); - this.videoEnabled = media.pipe( + this.videoEnabled$ = media$.pipe( map((m) => m?.cameraTrack?.isMuted === false), ); } public toggleFitContain(): void { - this._cropVideo.next(!this._cropVideo.value); + this._cropVideo$.next(!this._cropVideo$.value); } public get local(): boolean { @@ -410,7 +410,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel { /** * Whether the video should be mirrored. */ - public readonly mirror = this.video.pipe( + public readonly mirror$ = this.video$.pipe( switchMap((v) => { const track = v?.publication?.track; if (!(track instanceof LocalTrack)) return of(false); @@ -428,17 +428,17 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel { * Whether to show this tile in a highly visible location near the start of * the grid. */ - public readonly alwaysShow = alwaysShowSelf.value; + public readonly alwaysShow$ = alwaysShowSelf.value$; public readonly setAlwaysShow = alwaysShowSelf.setValue; public constructor( id: string, member: RoomMember | undefined, - participant: Observable, + participant$: Observable, encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, ) { - super(id, member, participant, encryptionSystem, livekitRoom); + super(id, member, participant$, encryptionSystem, livekitRoom); } } @@ -446,18 +446,18 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel { * A remote participant's user media. */ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel { - private readonly locallyMutedToggle = new Subject(); - private readonly localVolumeAdjustment = new Subject(); - private readonly localVolumeCommit = new Subject(); + 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: Observable = merge( - this.locallyMutedToggle.pipe(map(() => "toggle mute" as const)), - this.localVolumeAdjustment, - this.localVolumeCommit.pipe(map(() => "commit" as const)), + public readonly localVolume$: Observable = 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) { @@ -487,7 +487,7 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel { /** * Whether this participant's audio is disabled. */ - public readonly locallyMuted: Observable = this.localVolume.pipe( + public readonly locallyMuted$: Observable = this.localVolume$.pipe( map((volume) => volume === 0), this.scope.state(), ); @@ -495,29 +495,29 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: Observable, + participant$: Observable, encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, ) { - super(id, member, participant, encryptionSystem, livekitRoom); + super(id, member, participant$, encryptionSystem, livekitRoom); // Sync the local volume with LiveKit combineLatest([ - participant, - this.localVolume.pipe(this.scope.bind()), + participant$, + this.localVolume$.pipe(this.scope.bind()), ]).subscribe(([p, volume]) => p && p.setVolume(volume)); } public toggleLocallyMuted(): void { - this.locallyMutedToggle.next(); + this.locallyMutedToggle$.next(); } public setLocalVolume(value: number): void { - this.localVolumeAdjustment.next(value); + this.localVolumeAdjustment$.next(value); } public commitLocalVolume(): void { - this.localVolumeCommit.next(); + this.localVolumeCommit$.next(); } } @@ -528,7 +528,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: Observable, + participant$: Observable, encryptionSystem: EncryptionSystem, livekitRoom: LivekitRoom, public readonly local: boolean, @@ -536,7 +536,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel { super( id, member, - participant, + participant$, encryptionSystem, Track.Source.ScreenShareAudio, Track.Source.ScreenShare, diff --git a/src/state/ObservableScope.ts b/src/state/ObservableScope.ts index 5a2e0e9a..254fc03f 100644 --- a/src/state/ObservableScope.ts +++ b/src/state/ObservableScope.ts @@ -19,9 +19,9 @@ type MonoTypeOperator = (o: Observable) => Observable; * A scope which limits the execution lifetime of its bound Observables. */ export class ObservableScope { - private readonly ended = new Subject(); + private readonly ended$ = new Subject(); - private readonly bindImpl: MonoTypeOperator = takeUntil(this.ended); + private readonly bindImpl: MonoTypeOperator = takeUntil(this.ended$); /** * Binds an Observable to this scope, so that it completes when the scope @@ -31,8 +31,8 @@ export class ObservableScope { return this.bindImpl; } - private readonly stateImpl: MonoTypeOperator = (o) => - o.pipe( + private readonly stateImpl: MonoTypeOperator = (o$) => + o$.pipe( this.bind(), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: false }), @@ -51,7 +51,7 @@ export class ObservableScope { * Ends the scope, causing any bound Observables to complete. */ public end(): void { - this.ended.next(); - this.ended.complete(); + this.ended$.next(); + this.ended$.complete(); } } diff --git a/src/state/TileStore.ts b/src/state/TileStore.ts index cd269944..4d6878b6 100644 --- a/src/state/TileStore.ts +++ b/src/state/TileStore.ts @@ -18,31 +18,31 @@ function debugEntries(entries: GridTileData[]): string[] { } let DEBUG_ENABLED = false; -debugTileLayout.value.subscribe((value) => (DEBUG_ENABLED = value)); +debugTileLayout.value$.subscribe((value) => (DEBUG_ENABLED = value)); class SpotlightTileData { - private readonly media_: BehaviorSubject; + private readonly media$: BehaviorSubject; public get media(): MediaViewModel[] { - return this.media_.value; + return this.media$.value; } public set media(value: MediaViewModel[]) { - this.media_.next(value); + this.media$.next(value); } - private readonly maximised_: BehaviorSubject; + private readonly maximised$: BehaviorSubject; public get maximised(): boolean { - return this.maximised_.value; + return this.maximised$.value; } public set maximised(value: boolean) { - this.maximised_.next(value); + this.maximised$.next(value); } public readonly vm: SpotlightTileViewModel; public constructor(media: MediaViewModel[], maximised: boolean) { - this.media_ = new BehaviorSubject(media); - this.maximised_ = new BehaviorSubject(maximised); - this.vm = new SpotlightTileViewModel(this.media_, this.maximised_); + this.media$ = new BehaviorSubject(media); + this.maximised$ = new BehaviorSubject(maximised); + this.vm = new SpotlightTileViewModel(this.media$, this.maximised$); } public destroy(): void { @@ -51,19 +51,19 @@ class SpotlightTileData { } class GridTileData { - private readonly media_: BehaviorSubject; + private readonly media$: BehaviorSubject; public get media(): UserMediaViewModel { - return this.media_.value; + return this.media$.value; } public set media(value: UserMediaViewModel) { - this.media_.next(value); + this.media$.next(value); } public readonly vm: GridTileViewModel; public constructor(media: UserMediaViewModel) { - this.media_ = new BehaviorSubject(media); - this.vm = new GridTileViewModel(this.media_); + this.media$ = new BehaviorSubject(media); + this.vm = new GridTileViewModel(this.media$); } public destroy(): void { @@ -123,7 +123,10 @@ export class TileStoreBuilder { "speaking" in this.prevSpotlight.media[0] && this.prevSpotlight.media[0]; - private readonly prevGridByMedia = new Map( + private readonly prevGridByMedia: Map< + MediaViewModel, + [GridTileData, number] + > = new Map( this.prevGrid.map((entry, i) => [entry.media, [entry, i]] as const), ); diff --git a/src/state/TileViewModel.ts b/src/state/TileViewModel.ts index 612d7033..5815df54 100644 --- a/src/state/TileViewModel.ts +++ b/src/state/TileViewModel.ts @@ -18,15 +18,15 @@ function createId(): string { export class GridTileViewModel extends ViewModel { public readonly id = createId(); - public constructor(public readonly media: Observable) { + public constructor(public readonly media$: Observable) { super(); } } export class SpotlightTileViewModel extends ViewModel { public constructor( - public readonly media: Observable, - public readonly maximised: Observable, + public readonly media$: Observable, + public readonly maximised$: Observable, ) { super(); } diff --git a/src/state/observeSpeaker.test.ts b/src/state/observeSpeaker.test.ts index daa5f033..2a73482c 100644 --- a/src/state/observeSpeaker.test.ts +++ b/src/state/observeSpeaker.test.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { describe, test } from "vitest"; import { withTestScheduler } from "../utils/test"; -import { observeSpeaker } from "./observeSpeaker"; +import { observeSpeaker$ } from "./observeSpeaker"; const yesNo = { y: true, @@ -22,40 +22,36 @@ describe("observeSpeaker", () => { // should default to false when no input is given const speakingInputMarbles = ""; withTestScheduler(({ hot, expectObservable }) => { - expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( - expectedOutputMarbles, - yesNo, - ); + expectObservable( + observeSpeaker$(hot(speakingInputMarbles, yesNo)), + ).toBe(expectedOutputMarbles, yesNo); }); }); test("after no speaking", () => { const speakingInputMarbles = "n"; withTestScheduler(({ hot, expectObservable }) => { - expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( - expectedOutputMarbles, - yesNo, - ); + expectObservable( + observeSpeaker$(hot(speakingInputMarbles, yesNo)), + ).toBe(expectedOutputMarbles, yesNo); }); }); test("with speaking for 1ms", () => { const speakingInputMarbles = "y n"; withTestScheduler(({ hot, expectObservable }) => { - expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( - expectedOutputMarbles, - yesNo, - ); + expectObservable( + observeSpeaker$(hot(speakingInputMarbles, yesNo)), + ).toBe(expectedOutputMarbles, yesNo); }); }); test("with speaking for 999ms", () => { const speakingInputMarbles = "y 999ms n"; withTestScheduler(({ hot, expectObservable }) => { - expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( - expectedOutputMarbles, - yesNo, - ); + expectObservable( + observeSpeaker$(hot(speakingInputMarbles, yesNo)), + ).toBe(expectedOutputMarbles, yesNo); }); }); @@ -63,20 +59,18 @@ describe("observeSpeaker", () => { const speakingInputMarbles = "y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n"; withTestScheduler(({ hot, expectObservable }) => { - expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( - expectedOutputMarbles, - yesNo, - ); + expectObservable( + observeSpeaker$(hot(speakingInputMarbles, yesNo)), + ).toBe(expectedOutputMarbles, yesNo); }); }); test("with consecutive speaking then stops speaking", () => { const speakingInputMarbles = "y y y y y y y y y y n"; withTestScheduler(({ hot, expectObservable }) => { - expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( - expectedOutputMarbles, - yesNo, - ); + expectObservable( + observeSpeaker$(hot(speakingInputMarbles, yesNo)), + ).toBe(expectedOutputMarbles, yesNo); }); }); }); @@ -87,10 +81,9 @@ describe("observeSpeaker", () => { const speakingInputMarbles = " y"; const expectedOutputMarbles = "n 999ms y"; withTestScheduler(({ hot, expectObservable }) => { - expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( - expectedOutputMarbles, - yesNo, - ); + expectObservable( + observeSpeaker$(hot(speakingInputMarbles, yesNo)), + ).toBe(expectedOutputMarbles, yesNo); }); }); @@ -98,10 +91,9 @@ describe("observeSpeaker", () => { const speakingInputMarbles = " y 1s n "; const expectedOutputMarbles = "n 999ms y 60s n"; withTestScheduler(({ hot, expectObservable }) => { - expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( - expectedOutputMarbles, - yesNo, - ); + expectObservable( + observeSpeaker$(hot(speakingInputMarbles, yesNo)), + ).toBe(expectedOutputMarbles, yesNo); }); }); @@ -109,10 +101,9 @@ describe("observeSpeaker", () => { const speakingInputMarbles = " y 5s n "; const expectedOutputMarbles = "n 999ms y 64s n"; withTestScheduler(({ hot, expectObservable }) => { - expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe( - expectedOutputMarbles, - yesNo, - ); + expectObservable( + observeSpeaker$(hot(speakingInputMarbles, yesNo)), + ).toBe(expectedOutputMarbles, yesNo); }); }); }); diff --git a/src/state/observeSpeaker.ts b/src/state/observeSpeaker.ts index cce43ef9..8817af25 100644 --- a/src/state/observeSpeaker.ts +++ b/src/state/observeSpeaker.ts @@ -18,16 +18,16 @@ import { * Require 1 second of continuous speaking to become a speaker, and 60 second of * continuous silence to stop being considered a speaker */ -export function observeSpeaker( - isSpeakingObservable: Observable, +export function observeSpeaker$( + isSpeakingObservable$: Observable, ): Observable { - const distinct = isSpeakingObservable.pipe(distinctUntilChanged()); + const distinct$ = isSpeakingObservable$.pipe(distinctUntilChanged()); - return distinct.pipe( + return distinct$.pipe( // Either change to the new value after the timer or re-emit the same value if it toggles back // (audit will return the latest (toggled back) value) before the timeout. audit((s) => - merge(timer(s ? 1000 : 60000), distinct.pipe(filter((s1) => s1 !== s))), + merge(timer(s ? 1000 : 60000), distinct$.pipe(filter((s1) => s1 !== s))), ), // Filter the re-emissions (marked as: | ) that happen if we toggle quickly (<1s) from false->true->false|->.. startWith(false), diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index 73c17527..8c6b2d9b 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -83,13 +83,13 @@ const UserMediaTile = forwardRef( ref, ) => { const { t } = useTranslation(); - const video = useObservableEagerState(vm.video); - const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning); - const encryptionStatus = useObservableEagerState(vm.encryptionStatus); - const audioEnabled = useObservableEagerState(vm.audioEnabled); - const videoEnabled = useObservableEagerState(vm.videoEnabled); - const speaking = useObservableEagerState(vm.speaking); - const cropVideo = useObservableEagerState(vm.cropVideo); + const video = useObservableEagerState(vm.video$); + const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning$); + const encryptionStatus = useObservableEagerState(vm.encryptionStatus$); + const audioEnabled = useObservableEagerState(vm.audioEnabled$); + const videoEnabled = useObservableEagerState(vm.videoEnabled$); + const speaking = useObservableEagerState(vm.speaking$); + const cropVideo = useObservableEagerState(vm.cropVideo$); const onSelectFitContain = useCallback( (e: Event) => { e.preventDefault(); @@ -198,8 +198,8 @@ interface LocalUserMediaTileProps extends TileProps { const LocalUserMediaTile = forwardRef( ({ vm, onOpenProfile, ...props }, ref) => { const { t } = useTranslation(); - const mirror = useObservableEagerState(vm.mirror); - const alwaysShow = useObservableEagerState(vm.alwaysShow); + const mirror = useObservableEagerState(vm.mirror$); + const alwaysShow = useObservableEagerState(vm.alwaysShow$); const latestAlwaysShow = useLatest(alwaysShow); const onSelectAlwaysShow = useCallback( (e: Event) => { @@ -249,8 +249,8 @@ const RemoteUserMediaTile = forwardRef< RemoteUserMediaTileProps >(({ vm, ...props }, ref) => { const { t } = useTranslation(); - const locallyMuted = useObservableEagerState(vm.locallyMuted); - const localVolume = useObservableEagerState(vm.localVolume); + const locallyMuted = useObservableEagerState(vm.locallyMuted$); + const localVolume = useObservableEagerState(vm.localVolume$); const onSelectMute = useCallback( (e: Event) => { e.preventDefault(); @@ -316,7 +316,7 @@ export const GridTile = forwardRef( ({ vm, onOpenProfile, ...props }, theirRef) => { const ourRef = useRef(null); const ref = useMergedRefs(ourRef, theirRef); - const media = useObservableEagerState(vm.media); + const media = useObservableEagerState(vm.media$); const displayName = useDisplayName(media); if (media instanceof LocalUserMediaViewModel) { diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index a1c3d46f..c72bad81 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -72,7 +72,7 @@ const SpotlightLocalUserMediaItem = forwardRef< HTMLDivElement, SpotlightLocalUserMediaItemProps >(({ vm, ...props }, ref) => { - const mirror = useObservableEagerState(vm.mirror); + const mirror = useObservableEagerState(vm.mirror$); return ; }); @@ -86,8 +86,8 @@ const SpotlightUserMediaItem = forwardRef< HTMLDivElement, SpotlightUserMediaItemProps >(({ vm, ...props }, ref) => { - const videoEnabled = useObservableEagerState(vm.videoEnabled); - const cropVideo = useObservableEagerState(vm.cropVideo); + const videoEnabled = useObservableEagerState(vm.videoEnabled$); + const cropVideo = useObservableEagerState(vm.cropVideo$); const baseProps: SpotlightUserMediaItemBaseProps & RefAttributes = { @@ -110,7 +110,7 @@ interface SpotlightItemProps { vm: MediaViewModel; targetWidth: number; targetHeight: number; - intersectionObserver: Observable; + intersectionObserver$: Observable; /** * Whether this item should act as a scroll snapping point. */ @@ -124,7 +124,7 @@ const SpotlightItem = forwardRef( vm, targetWidth, targetHeight, - intersectionObserver, + intersectionObserver$, snap, "aria-hidden": ariaHidden, }, @@ -133,15 +133,15 @@ const SpotlightItem = forwardRef( const ourRef = useRef(null); const ref = useMergedRefs(ourRef, theirRef); const displayName = useDisplayName(vm); - const video = useObservableEagerState(vm.video); - const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning); - const encryptionStatus = useObservableEagerState(vm.encryptionStatus); + const video = useObservableEagerState(vm.video$); + const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning$); + const encryptionStatus = useObservableEagerState(vm.encryptionStatus$); // Hook this item up to the intersection observer useEffect(() => { const element = ourRef.current!; let prevIo: IntersectionObserver | null = null; - const subscription = intersectionObserver.subscribe((io) => { + const subscription = intersectionObserver$.subscribe((io) => { prevIo?.unobserve(element); io.observe(element); prevIo = io; @@ -150,7 +150,7 @@ const SpotlightItem = forwardRef( subscription.unsubscribe(); prevIo?.unobserve(element); }; - }, [intersectionObserver]); + }, [intersectionObserver$]); const baseProps: SpotlightItemBaseProps & RefAttributes = { ref, @@ -208,10 +208,10 @@ export const SpotlightTile = forwardRef( theirRef, ) => { const { t } = useTranslation(); - const [ourRef, root] = useObservableRef(null); + const [ourRef, root$] = useObservableRef(null); const ref = useMergedRefs(ourRef, theirRef); - const maximised = useObservableEagerState(vm.maximised); - const media = useObservableEagerState(vm.media); + const maximised = useObservableEagerState(vm.maximised$); + const media = useObservableEagerState(vm.media$); const [visibleId, setVisibleId] = useState( media[0]?.id, ); @@ -225,9 +225,9 @@ export const SpotlightTile = forwardRef( // hooked up to the root element and the items. Because the items will run // their effects before their parent does, we need to do this dance with an // Observable to actually give them the intersection observer. - const intersectionObserver = useInitial>( + const intersectionObserver$ = useInitial>( () => - root.pipe( + root$.pipe( map( (r) => new IntersectionObserver( @@ -295,7 +295,7 @@ export const SpotlightTile = forwardRef( vm={vm} targetWidth={targetWidth} targetHeight={targetHeight} - intersectionObserver={intersectionObserver} + intersectionObserver$={intersectionObserver$} // This is how we get the container to scroll to the right media // when the previous/next buttons are clicked: we temporarily // remove all scroll snap points except for just the one media diff --git a/src/utils/observable.ts b/src/utils/observable.ts index a54c0293..977bdf79 100644 --- a/src/utils/observable.ts +++ b/src/utils/observable.ts @@ -15,10 +15,10 @@ const nothing = Symbol("nothing"); * callback will not be invoked. */ export function finalizeValue(callback: (finalValue: T) => void) { - return (source: Observable): Observable => + return (source$: Observable): Observable => defer(() => { let finalValue: T | typeof nothing = nothing; - return source.pipe( + return source$.pipe( tap((value) => (finalValue = value)), finalize(() => { if (finalValue !== nothing) callback(finalValue); @@ -35,6 +35,6 @@ export function accumulate( initial: State, update: (state: State, event: Event) => State, ) { - return (events: Observable): Observable => - events.pipe(scan(update, initial), startWith(initial)); + return (events$: Observable): Observable => + events$.pipe(scan(update, initial), startWith(initial)); } diff --git a/src/utils/test.ts b/src/utils/test.ts index 1cd21f01..db0d8959 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -77,14 +77,14 @@ export function withTestScheduler( continuation({ ...helpers, schedule(marbles, actions) { - const actionsObservable = helpers + const actionsObservable$ = helpers .cold(marbles) .pipe(map((value) => actions[value]())); const results = Object.fromEntries( Object.keys(actions).map((value) => [value, undefined] as const), ); // Run the actions and verify that none of them error - helpers.expectObservable(actionsObservable).toBe(marbles, results); + helpers.expectObservable(actionsObservable$).toBe(marbles, results); }, }), ); @@ -157,16 +157,16 @@ export function mockMatrixRoom(room: Partial): MatrixRoom { export function mockLivekitRoom( room: Partial, { - remoteParticipants, - }: { remoteParticipants?: Observable } = {}, + remoteParticipants$, + }: { remoteParticipants$?: Observable } = {}, ): LivekitRoom { const livekitRoom = { ...mockEmitter(), ...room, } as Partial as LivekitRoom; - if (remoteParticipants) { + if (remoteParticipants$) { livekitRoom.remoteParticipants = new Map(); - remoteParticipants.subscribe((newRemoteParticipants) => { + remoteParticipants$.subscribe((newRemoteParticipants) => { livekitRoom.remoteParticipants.clear(); newRemoteParticipants.forEach((p) => { livekitRoom.remoteParticipants.set(p.identity, p); @@ -238,7 +238,7 @@ export async function withRemoteMedia( { kind: E2eeType.PER_PARTICIPANT, }, - mockLivekitRoom({}, { remoteParticipants: of([remoteParticipant]) }), + mockLivekitRoom({}, { remoteParticipants$: of([remoteParticipant]) }), ); try { await continuation(vm); @@ -277,9 +277,9 @@ export class MockRTCSession extends TypedEventEmitter< } public withMemberships( - rtcMembers: Observable[]>, + rtcMembers$: Observable[]>, ): MockRTCSession { - rtcMembers.subscribe((m) => { + rtcMembers$.subscribe((m) => { const old = this.memberships; // always prepend the local participant const updated = [this.localMembership, ...(m as CallMembership[])]; From 2b6acb9cce8e76dd4507fe499171bc17c4ffcedd Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 17 Dec 2024 10:17:37 +0000 Subject: [PATCH 17/19] Use afterEach from vitest for consistency (#2908) This slipped through the PR review. Whilst not harmful, for consistency we use it from vitest. --- src/Modal.test.tsx | 3 +-- src/room/CallEventAudioRenderer.test.tsx | 2 +- src/room/ReactionAudioRenderer.test.tsx | 2 +- src/room/ReactionsOverlay.test.tsx | 3 +-- src/useAudioContext.test.tsx | 3 +-- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Modal.test.tsx b/src/Modal.test.tsx index bb6fb0f7..6368c7d9 100644 --- a/src/Modal.test.tsx +++ b/src/Modal.test.tsx @@ -5,10 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { expect, test } from "vitest"; +import { expect, test, afterEach } from "vitest"; import { render } from "@testing-library/react"; import { type ReactNode, useState } from "react"; -import { afterEach } from "node:test"; import userEvent from "@testing-library/user-event"; import { Modal } from "./Modal"; diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index 6868de49..cc7f4eea 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -13,11 +13,11 @@ import { type MockedFunction, test, vitest, + afterEach, } from "vitest"; import { type MatrixClient } from "matrix-js-sdk/src/client"; import { ConnectionState } from "livekit-client"; import { BehaviorSubject, of } from "rxjs"; -import { afterEach } from "node:test"; import { act, type ReactNode } from "react"; import { type CallMembership, diff --git a/src/room/ReactionAudioRenderer.test.tsx b/src/room/ReactionAudioRenderer.test.tsx index 0ab283a9..afa2c6ff 100644 --- a/src/room/ReactionAudioRenderer.test.tsx +++ b/src/room/ReactionAudioRenderer.test.tsx @@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details. import { render } from "@testing-library/react"; import { afterAll, + afterEach, beforeEach, expect, test, @@ -17,7 +18,6 @@ import { } from "vitest"; import { TooltipProvider } from "@vector-im/compound-web"; import { act, type ReactNode } from "react"; -import { afterEach } from "node:test"; import { MockRoom, diff --git a/src/room/ReactionsOverlay.test.tsx b/src/room/ReactionsOverlay.test.tsx index 8ea17178..5c3f8bf9 100644 --- a/src/room/ReactionsOverlay.test.tsx +++ b/src/room/ReactionsOverlay.test.tsx @@ -6,10 +6,9 @@ Please see LICENSE in the repository root for full details. */ import { render } from "@testing-library/react"; -import { expect, test } from "vitest"; +import { expect, test, afterEach } from "vitest"; import { TooltipProvider } from "@vector-im/compound-web"; import { act, type ReactNode } from "react"; -import { afterEach } from "node:test"; import { MockRoom, diff --git a/src/useAudioContext.test.tsx b/src/useAudioContext.test.tsx index 2fda4add..9f97f1b6 100644 --- a/src/useAudioContext.test.tsx +++ b/src/useAudioContext.test.tsx @@ -5,10 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { expect, test, vitest } from "vitest"; +import { expect, test, vitest, afterEach } from "vitest"; import { type FC } from "react"; import { render } from "@testing-library/react"; -import { afterEach } from "node:test"; import userEvent from "@testing-library/user-event"; import { deviceStub, MediaDevicesContext } from "./livekit/MediaDevicesContext"; From 53fff37d5d36051a8372d006358fcf523170263c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 17 Dec 2024 16:44:50 +0000 Subject: [PATCH 18/19] Use AutoDiscovery.getRawClientConfig() instead of MatrixClient.getClientWellKnown() (#2906) I'm open to suggestions on what is sensible to test around this... --- src/rtcSessionHelper.test.ts | 13 ++++++++++++- src/rtcSessionHelpers.ts | 36 +++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/rtcSessionHelper.test.ts b/src/rtcSessionHelper.test.ts index 7df9f1b3..797312f6 100644 --- a/src/rtcSessionHelper.test.ts +++ b/src/rtcSessionHelper.test.ts @@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details. import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { expect, test, vi } from "vitest"; +import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery"; import { enterRTCSession } from "../src/rtcSessionHelpers"; import { mockConfig } from "./utils/test"; @@ -36,11 +37,21 @@ test("It joins the correct Session", async () => { mockConfig({ livekit: { livekit_service_url: "http://my-default-service-url.com" }, }); + + vi.spyOn(AutoDiscovery, "getRawClientConfig").mockImplementation( + async (domain) => { + if (domain === "example.org") { + return Promise.resolve(clientWellKnown); + } + return Promise.resolve({}); + }, + ); + const mockedSession = vi.mocked({ room: { roomId: "roomId", client: { - getClientWellKnown: vi.fn().mockReturnValue(clientWellKnown), + getDomain: vi.fn().mockReturnValue("example.org"), }, }, memberships: [], diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index f1c7eb8c..3aa2fbbd 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -13,6 +13,7 @@ import { isLivekitFocus, isLivekitFocusConfig, } from "matrix-js-sdk/src/matrixrtc/LivekitFocus"; +import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery"; import { PosthogAnalytics } from "./analytics/PosthogAnalytics"; import { Config } from "./config/Config"; @@ -43,19 +44,28 @@ async function makePreferredLivekitFoci( preferredFoci.push(focusInUse); } - // Prioritize the client well known over the configured sfu. - const wellKnownFoci = - rtcSession.room.client.getClientWellKnown()?.[FOCI_WK_KEY]; - if (Array.isArray(wellKnownFoci)) { - preferredFoci.push( - ...wellKnownFoci - .filter((f) => !!f) - .filter(isLivekitFocusConfig) - .map((wellKnownFocus) => { - logger.log("Adding livekit focus from well known: ", wellKnownFocus); - return { ...wellKnownFocus, livekit_alias: livekitAlias }; - }), - ); + // Prioritize the .well-known/matrix/client, if available, over the configured SFU + const domain = rtcSession.room.client.getDomain(); + if (domain) { + // we use AutoDiscovery instead of relying on the MatrixClient having already + // been fully configured and started + const wellKnownFoci = (await AutoDiscovery.getRawClientConfig(domain))?.[ + FOCI_WK_KEY + ]; + if (Array.isArray(wellKnownFoci)) { + preferredFoci.push( + ...wellKnownFoci + .filter((f) => !!f) + .filter(isLivekitFocusConfig) + .map((wellKnownFocus) => { + logger.log( + "Adding livekit focus from well known: ", + wellKnownFocus, + ); + return { ...wellKnownFocus, livekit_alias: livekitAlias }; + }), + ); + } } const urlFromConf = Config.get().livekit?.livekit_service_url; From 6b8dddfaaad68fd0bd1f4f5c0c2fb7b351775ffe Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 18 Dec 2024 09:35:42 +0000 Subject: [PATCH 19/19] Bump matrix-js-sdk for bundle improvements (#2911) Brings in updated version of matrix-sdk-crypto-wasm which helps to reduce bundle size and lazy loading. Full diff https://github.com/matrix-org/matrix-js-sdk/compare/d1de32ea2773df4c6f8a956678bbd19b6d022475...e4182eb75227c283a18704727021e99ced72868d --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5fdd147f..d3b8da40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1961,10 +1961,10 @@ dependencies: "@bufbuild/protobuf" "^1.10.0" -"@matrix-org/matrix-sdk-crypto-wasm@^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.0.0.tgz#c49a1a0d1e367d3c00a2144a4ab23caee0b1eec2" - integrity sha512-a7NUH8Kjc8hwzNCPpkOGXoceFqWJiWvA8OskXeDrKyODJuDz4yKrZ/nvgaVRfQe45Ab5UC1ZXYqaME+ChlJuqg== +"@matrix-org/matrix-sdk-crypto-wasm@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.0.0.tgz#e3a5150ccbb21d5e98ee3882e7057b9f17fb962a" + integrity sha512-nkkXAxUIk9UTso4TbU6Bgqsv/rJShXQXRx0ti/W+AWXHJ2HoH4sL5LsXkc7a8yYGn8tyXqxGPsYA1UeHqLwm0Q== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -6568,10 +6568,10 @@ matrix-events-sdk@0.0.1: matrix-js-sdk@matrix-org/matrix-js-sdk#develop: version "34.13.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d1de32ea2773df4c6f8a956678bbd19b6d022475" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e4182eb75227c283a18704727021e99ced72868d" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^11.0.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^12.0.0" "@matrix-org/olm" "3.2.15" another-json "^0.2.0" bs58 "^6.0.0"