From 44b6db6ff1156d4cb1a93c228fb87416380bb519 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Apr 2026 11:47:24 +0100 Subject: [PATCH 001/100] Replace LayoutToggle with compound-web Switch component --- package.json | 2 +- pnpm-lock.yaml | 11 +++-- src/components/CallFooter.tsx | 19 ++++++-- src/room/LayoutToggle.module.css | 79 ------------------------------- src/room/LayoutToggle.stories.tsx | 25 ---------- src/room/LayoutToggle.tsx | 59 ----------------------- 6 files changed, 22 insertions(+), 173 deletions(-) delete mode 100644 src/room/LayoutToggle.module.css delete mode 100644 src/room/LayoutToggle.stories.tsx delete mode 100644 src/room/LayoutToggle.tsx diff --git a/package.json b/package.json index c0f4d505..5fa4fed2 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^10.0.0", - "@vector-im/compound-web": "^9.0.0", + "@vector-im/compound-web": "element-hq/compound-web#e7c91ef18e20f2fc70069696f4414018361ac512", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^4.0.18", "babel-plugin-transform-vite-meta-env": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c800b0c2..07bc5fbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,8 +150,8 @@ importers: specifier: ^10.0.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.5) '@vector-im/compound-web': - specifier: ^9.0.0 - version: 9.2.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: element-hq/compound-web#e7c91ef18e20f2fc70069696f4414018361ac512 + version: https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vitejs/plugin-react': specifier: ^4.0.1 version: 4.7.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -3037,8 +3037,9 @@ packages: react: optional: true - '@vector-im/compound-web@9.2.0': - resolution: {integrity: sha512-jHbABGEQ2yqNtm5xRIkklQs198VEfSk9AJQolI+e4WSJ0xg8Ozyv9t9KIuKQAmjdSV9aow5G6hDE861XB6DQgw==} + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512': + resolution: {tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512} + version: 9.2.1 peerDependencies: '@fontsource/inconsolata': ^5 '@fontsource/inter': ^5 @@ -9473,7 +9474,7 @@ snapshots: '@types/react': 19.2.14 react: 19.2.5 - '@vector-im/compound-web@9.2.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@fontsource/inconsolata': 5.2.8 diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index 4e728d3b..21ff52c6 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -8,6 +8,12 @@ Please see LICENSE in the repository root for full details. import { type FC, type JSX, type Ref, useMemo } from "react"; import classNames from "classnames"; import { BehaviorSubject } from "rxjs"; +import { Switch } from "@vector-im/compound-web"; +import { t } from "i18next"; +import { + SpotlightIcon, + GridIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import LogoMark from "../icons/LogoMark.svg?react"; import LogoType from "../icons/LogoType.svg?react"; @@ -23,7 +29,6 @@ import { type ReactionData, } from "../button"; import styles from "./CallFooter.module.css"; -import { LayoutToggle } from "../room/LayoutToggle"; import { type GridMode } from "../state/CallViewModel/CallViewModel"; export interface AudioOutputSwitcher { @@ -232,10 +237,16 @@ export const CallFooter: FC = ({ {!hideControls &&
{buttons}
} {setLayoutMode && layoutMode && showLayoutSwitcher && ( - )} diff --git a/src/room/LayoutToggle.module.css b/src/room/LayoutToggle.module.css deleted file mode 100644 index d9ae5813..00000000 --- a/src/room/LayoutToggle.module.css +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -.toggle { - padding: 2px; - border: 1px solid var(--cpd-color-border-interactive-secondary); - border-radius: var(--cpd-radius-pill-effect); - background: var(--cpd-color-bg-canvas-default); - display: flex; - position: relative; -} - -.toggle input { - appearance: none; - /* Safari puts a margin on these, which is not removed via appearance: none */ - margin: 0; - block-size: var(--cpd-space-11x); - inline-size: var(--cpd-space-11x); - cursor: pointer; - border-radius: var(--cpd-radius-pill-effect); - background: var(--cpd-color-bg-action-secondary-rest); - box-shadow: var(--small-drop-shadow); - transition: background-color 0.1s; -} - -.toggle svg { - display: block; - position: absolute; - padding: calc(2.5 * var(--cpd-space-1x)); - pointer-events: none; - color: var(--cpd-color-icon-primary); - transition: color 0.1s; -} - -.toggle svg:nth-child(2) { - inset-inline-start: 2px; -} - -.toggle svg:nth-child(4) { - inset-inline-end: 2px; -} - -@media (hover: hover) { - .toggle input:hover { - background: var(--cpd-color-bg-action-secondary-hovered); - box-shadow: none; - } -} - -.toggle input:active { - background: var(--cpd-color-bg-action-secondary-pressed); - box-shadow: none; -} - -.toggle input:checked { - background: var(--cpd-color-bg-action-primary-rest); -} - -.toggle input:checked + svg { - color: var(--cpd-color-icon-on-solid-primary); -} - -@media (hover: hover) { - .toggle input:checked:hover { - background: var(--cpd-color-bg-action-primary-hovered); - } -} - -.toggle input:checked:active { - background: var(--cpd-color-bg-action-primary-pressed); -} - -.toggle input:first-child { - margin-inline-end: 5px; -} diff --git a/src/room/LayoutToggle.stories.tsx b/src/room/LayoutToggle.stories.tsx deleted file mode 100644 index 72a2ffad..00000000 --- a/src/room/LayoutToggle.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2026 Element Creations Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { fn } from "storybook/test"; - -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { LayoutToggle } from "./LayoutToggle"; - -const meta = { - component: LayoutToggle, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - layout: "grid", - setLayout: fn(), - }, -}; diff --git a/src/room/LayoutToggle.tsx b/src/room/LayoutToggle.tsx deleted file mode 100644 index 98ed91d3..00000000 --- a/src/room/LayoutToggle.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { type ChangeEvent, type FC, useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import { Tooltip } from "@vector-im/compound-web"; -import { - SpotlightIcon, - GridIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; -import classNames from "classnames"; - -import styles from "./LayoutToggle.module.css"; - -export type Layout = "spotlight" | "grid"; - -type Props = { - layout: Layout; - setLayout: (layout: Layout) => void; - className?: string; -}; - -export const LayoutToggle: FC = ({ layout, setLayout, className }) => { - const { t } = useTranslation(); - - const onChange = useCallback( - (e: ChangeEvent) => setLayout(e.target.value as Layout), - [setLayout], - ); - - return ( -
- - - - - - - - - - ); -}; From d8be06974767e36598f71003121b80b5c7673a00 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Apr 2026 11:54:24 +0100 Subject: [PATCH 002/100] Fix type --- locales/en/app.json | 1 + package.json | 2 +- src/components/CallFooter.tsx | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 5398930f..2545bb4b 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -143,6 +143,7 @@ "text": "This call already exists, would you like to join?", "title": "Join existing call?" }, + "layout_switch_label": "Layout", "layout_grid_label": "Grid", "layout_spotlight_label": "Spotlight", "lobby": { diff --git a/package.json b/package.json index 5fa4fed2..f27cf8f4 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^10.0.0", - "@vector-im/compound-web": "element-hq/compound-web#e7c91ef18e20f2fc70069696f4414018361ac512", + "@vector-im/compound-web": "element-hq/compound-web#fc2e677326aaefec61ef74fb1d9de3c01eecfa7e", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^4.0.18", "babel-plugin-transform-vite-meta-env": "^1.0.3", diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index 21ff52c6..d10e4ecf 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -237,7 +237,8 @@ export const CallFooter: FC = ({ {!hideControls &&
{buttons}
} {setLayoutMode && layoutMode && showLayoutSwitcher && ( - + aria-label={t("layout_switch_label")} leftLabel={t("layout_spotlight_label")} leftValue="spotlight" leftIcon={SpotlightIcon} From 9b71070ef8e312bd17d1b0c71eda775c36cf4e30 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Apr 2026 11:57:13 +0100 Subject: [PATCH 003/100] publish lock changes --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07bc5fbc..06269963 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,8 +150,8 @@ importers: specifier: ^10.0.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.5) '@vector-im/compound-web': - specifier: element-hq/compound-web#e7c91ef18e20f2fc70069696f4414018361ac512 - version: https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: element-hq/compound-web#fc2e677326aaefec61ef74fb1d9de3c01eecfa7e + version: https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vitejs/plugin-react': specifier: ^4.0.1 version: 4.7.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -3037,8 +3037,8 @@ packages: react: optional: true - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512': - resolution: {tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512} + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e': + resolution: {tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e} version: 9.2.1 peerDependencies: '@fontsource/inconsolata': ^5 @@ -9474,7 +9474,7 @@ snapshots: '@types/react': 19.2.14 react: 19.2.5 - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@fontsource/inconsolata': 5.2.8 From 62751787ca0c4f95572d3feadd880297d3933e5e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Apr 2026 13:02:47 +0100 Subject: [PATCH 004/100] Use actual package --- package.json | 2 +- pnpm-lock.yaml | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index f27cf8f4..a8dc49fa 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^10.0.0", - "@vector-im/compound-web": "element-hq/compound-web#fc2e677326aaefec61ef74fb1d9de3c01eecfa7e", + "@vector-im/compound-web": "^9.3.0", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^4.0.18", "babel-plugin-transform-vite-meta-env": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06269963..41e3f943 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,8 +150,8 @@ importers: specifier: ^10.0.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.5) '@vector-im/compound-web': - specifier: element-hq/compound-web#fc2e677326aaefec61ef74fb1d9de3c01eecfa7e - version: https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^9.3.0 + version: 9.3.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vitejs/plugin-react': specifier: ^4.0.1 version: 4.7.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -3037,9 +3037,8 @@ packages: react: optional: true - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e': - resolution: {tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e} - version: 9.2.1 + '@vector-im/compound-web@9.3.0': + resolution: {integrity: sha512-Elu4Uw8RbfP6JaudQYkVibALYT6qpwubqfKhteTxIPWBWzSYM+P5T+B1uX+ra+grNcXwXUt2xfMxpqYQsAHgYA==} peerDependencies: '@fontsource/inconsolata': ^5 '@fontsource/inter': ^5 @@ -9474,7 +9473,7 @@ snapshots: '@types/react': 19.2.14 react: 19.2.5 - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@vector-im/compound-web@9.3.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@fontsource/inconsolata': 5.2.8 From 3bc8c0590d3d8c355cbc09f6e9727f7dc48f7861 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 24 Apr 2026 18:27:05 +0200 Subject: [PATCH 005/100] dev-tool: Add option to enable extended livekit logs --- src/App.tsx | 2 ++ src/LivekitLogLevelSync.tsx | 22 ++++++++++++++++++++++ src/settings/DeveloperSettingsTab.tsx | 19 +++++++++++++++++++ src/settings/settings.ts | 5 +++++ 4 files changed, 48 insertions(+) create mode 100644 src/LivekitLogLevelSync.tsx diff --git a/src/App.tsx b/src/App.tsx index b87f587c..e124b0bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ import { type AppViewModel } from "./state/AppViewModel"; import { MediaDevicesContext } from "./MediaDevicesContext"; import { getUrlParams, HeaderStyle } from "./UrlParams"; import { AppBar } from "./AppBar"; +import { LivekitLogLevelSync } from "./LivekitLogLevelSync.tsx"; const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route); @@ -81,6 +82,7 @@ export const App: FC = ({ vm }) => { const content = loaded ? ( + } diff --git a/src/LivekitLogLevelSync.tsx b/src/LivekitLogLevelSync.tsx new file mode 100644 index 00000000..5d3916d2 --- /dev/null +++ b/src/LivekitLogLevelSync.tsx @@ -0,0 +1,22 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +// Syncs the livekit log level with the "Enable extended Livekit logs" developer setting. +import { type FC, useEffect } from "react"; +import { setLogLevel } from "livekit-client"; + +import { useSetting, enableExtendedLivekitLogs } from "./settings/settings.ts"; + +export const LivekitLogLevelSync: FC = () => { + const [extendedLivekitLogs] = useSetting(enableExtendedLivekitLogs); + + useEffect(() => { + setLogLevel(extendedLivekitLogs ? "trace" : "info"); + }, [extendedLivekitLogs]); + + return <>; +}; diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index 9df6181f..0db5e2ef 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -43,6 +43,7 @@ import { matrixRTCMode as matrixRTCModeSetting, customLivekitUrl as customLivekitUrlSetting, MatrixRTCMode, + enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting, } from "./settings"; import styles from "./DeveloperSettingsTab.module.css"; import { useUrlParams } from "../UrlParams"; @@ -101,6 +102,10 @@ export const DeveloperSettingsTab: FC = ({ alwaysShowIphoneEarpieceSetting, ); + const [enableExtendedLivekitLogs, setEnableExtendedLivekitLogs] = useSetting( + enableExtendedLivekitLogsSetting, + ); + const [customLivekitUrlUpdateError, setCustomLivekitUrlUpdateError] = useState(null); const [customLivekitUrl, setCustomLivekitUrl] = useSetting( @@ -227,6 +232,20 @@ export const DeveloperSettingsTab: FC = ({ )} />{" "} + + ): void => { + setEnableExtendedLivekitLogs(event.target.checked); + }, + [setEnableExtendedLivekitLogs], + )} + />{" "} + e.preventDefault()} helpLabel={ diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 917c79f1..cf0d9d66 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -129,6 +129,11 @@ export const alwaysShowIphoneEarpiece = new Setting( false, ); +export const enableExtendedLivekitLogs = new Setting( + "extended-livekit-logs", + false, +); + export enum MatrixRTCMode { Legacy = "legacy", Compatibility = "compatibility", From 5aa45714bfa194a3224e0181a2e8970a0b693ff1 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Apr 2026 09:34:46 +0100 Subject: [PATCH 006/100] Snap updates --- locales/en/app.json | 2 +- .../ReactionToggleButton.test.tsx.snap | 10 +++---- .../GroupCallErrorBoundary.test.tsx.snap | 18 ++++++------ .../__snapshots__/InCallView.test.tsx.snap | 28 +++++++++---------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 2545bb4b..b51c6ed9 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -143,9 +143,9 @@ "text": "This call already exists, would you like to join?", "title": "Join existing call?" }, - "layout_switch_label": "Layout", "layout_grid_label": "Grid", "layout_spotlight_label": "Spotlight", + "layout_switch_label": "Layout", "lobby": { "ask_to_join": "Request to join call", "join_as_guest": "Join as guest", diff --git a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap index 608c1a0f..a1e319d9 100644 --- a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap +++ b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap @@ -10,7 +10,7 @@ exports[`Can close reaction dialog 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_bb_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -44,7 +44,7 @@ exports[`Can fully expand emoji picker 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_7m_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -75,7 +75,7 @@ exports[`Can lower hand 1`] = ` aria-expanded="false" aria-haspopup="true" aria-labelledby="_r_36_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="secondary" data-size="lg" role="button" @@ -109,7 +109,7 @@ exports[`Can open menu 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_0_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -140,7 +140,7 @@ exports[`Can raise hand 1`] = ` aria-expanded="false" aria-haspopup="true" aria-labelledby="_r_1j_" - class="_button_13vu4_8 raisedButton _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 raisedButton _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" diff --git a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap index 0d2d39bc..92a6fe54 100644 --- a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap +++ b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap @@ -134,7 +134,7 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = ` You were disconnected from the call.

-
{buttons}
} {setLayoutMode && layoutMode && showLayoutSwitcher && ( + name="layoutMode" aria-label={t("layout_switch_label")} leftLabel={t("layout_spotlight_label")} leftValue="spotlight" From 5db1a20f9f6e716f98a01b27d4ceff7eb50c84fe Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Apr 2026 09:29:12 +0100 Subject: [PATCH 008/100] update snap --- src/room/__snapshots__/InCallView.test.tsx.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap index e800fd8f..d9f768e7 100644 --- a/src/room/__snapshots__/InCallView.test.tsx.snap +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -403,6 +403,7 @@ exports[`InCallView > rendering > renders 1`] = ` > @@ -421,6 +422,7 @@ exports[`InCallView > rendering > renders 1`] = ` From 1025d23ac7adc57a8b243d48f331809f744550fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 08:35:17 +0200 Subject: [PATCH 009/100] Update dependency livekit-client to v2.18.9 (#3958) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 114 +++++++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5d35a6e..a3ad458e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,16 +47,16 @@ importers: version: 11.7.12 '@livekit/components-core': specifier: ^0.12.0 - version: 0.12.13(livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) + version: 0.12.13(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) '@livekit/components-react': specifier: ^2.0.0 - version: 2.9.20(livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1) + version: 2.9.20(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1) '@livekit/protocol': specifier: ^1.42.2 version: 1.45.6 '@livekit/track-processors': specifier: ^0.7.1 - version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22)) + version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22)) '@mediapipe/tasks-vision': specifier: ^0.10.18 version: 0.10.34 @@ -227,7 +227,7 @@ importers: version: 5.88.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.12.2)(typescript@5.9.3) livekit-client: specifier: ^2.18.1 - version: 2.18.8(@types/dom-mediacapture-record@1.0.22) + version: 2.18.9(@types/dom-mediacapture-record@1.0.22) lodash-es: specifier: ^4.17.21 version: 4.18.1 @@ -236,7 +236,7 @@ importers: version: 1.9.2 matrix-js-sdk: specifier: matrix-org/matrix-js-sdk#develop - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68 matrix-widget-api: specifier: ^1.16.1 version: 1.17.0 @@ -2995,8 +2995,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.59.1': - resolution: {integrity: sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==} + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -3013,8 +3013,8 @@ packages: resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.59.1': - resolution: {integrity: sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==} + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/tsconfig-utils@8.58.2': @@ -3029,8 +3029,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/tsconfig-utils@8.59.1': - resolution: {integrity: sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==} + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -3058,6 +3058,10 @@ packages: resolution: {integrity: sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.62.0': resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3079,8 +3083,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.59.1': - resolution: {integrity: sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==} + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -3105,8 +3109,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.59.1': - resolution: {integrity: sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==} + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -3124,12 +3128,13 @@ packages: resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.59.1': - resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==} + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@use-gesture/core@10.3.1': resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} @@ -5021,8 +5026,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - livekit-client@2.18.8: - resolution: {integrity: sha512-E+bSpnBVng/1xG4RfL1Q51dHUpBwL14Wix4sR5bS0djEzKMEtrxcUyhWLltdwQ0USf1t0PaxW6WL4oVb2s4Fsw==} + livekit-client@2.18.9: + resolution: {integrity: sha512-l0cADcxxBCWCBMtU9eWY6RpdbRfgA5c1/05yngQXo08mcy3VOttmSE2pNZ74k2B2zQym149g5/Y1B3vq2FWwlw==} peerDependencies: '@types/dom-mediacapture-record': ^1 @@ -5096,8 +5101,8 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d: - resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d} + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68: + resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68} version: 41.4.0 engines: {node: '>=22.0.0'} @@ -5995,6 +6000,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -8204,21 +8214,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@livekit/components-core@0.12.13(livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': + '@livekit/components-core@0.12.13(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': dependencies: '@floating-ui/dom': 1.7.4 - livekit-client: 2.18.8(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.9(@types/dom-mediacapture-record@1.0.22) loglevel: 1.9.1 rxjs: 7.8.2 tslib: 2.8.1 - '@livekit/components-react@2.9.20(livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)': + '@livekit/components-react@2.9.20(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)': dependencies: - '@livekit/components-core': 0.12.13(livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) + '@livekit/components-core': 0.12.13(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) clsx: 2.1.1 events: 3.3.0 jose: 6.2.2 - livekit-client: 2.18.8(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.9(@types/dom-mediacapture-record@1.0.22) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 @@ -8234,11 +8244,11 @@ snapshots: dependencies: '@bufbuild/protobuf': 1.10.1 - '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22))': + '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))': dependencies: '@mediapipe/tasks-vision': 0.10.34 '@types/dom-mediacapture-transform': 0.1.11 - livekit-client: 2.18.8(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.9(@types/dom-mediacapture-record@1.0.22) '@matrix-org/matrix-sdk-crypto-wasm@18.2.0': {} @@ -9501,10 +9511,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.59.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) - '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -9525,10 +9535,10 @@ snapshots: '@typescript-eslint/types': 8.59.0 '@typescript-eslint/visitor-keys': 8.59.0 - '@typescript-eslint/scope-manager@8.59.1': + '@typescript-eslint/scope-manager@8.59.2': dependencies: - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/visitor-keys': 8.59.1 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)': dependencies: @@ -9538,7 +9548,7 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.59.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -9562,6 +9572,8 @@ snapshots: '@typescript-eslint/types@8.59.1': {} + '@typescript-eslint/types@8.59.2': {} + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 5.62.0 @@ -9606,15 +9618,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.59.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/visitor-keys': 8.59.1 + '@typescript-eslint/project-service': 8.59.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.7.4 + semver: 7.8.0 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -9658,12 +9670,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.1(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.59.2(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.59.1 - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: @@ -9684,9 +9696,9 @@ snapshots: '@typescript-eslint/types': 8.59.0 eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.59.1': + '@typescript-eslint/visitor-keys@8.59.2': dependencies: - '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/types': 8.59.2 eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} @@ -10847,7 +10859,7 @@ snapshots: eslint-plugin-jest@29.15.2(@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.59.1(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -11907,7 +11919,7 @@ snapshots: lines-and-columns@1.2.4: {} - livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22): + livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22): dependencies: '@livekit/mutex': 1.1.1 '@livekit/protocol': 1.45.3 @@ -11983,7 +11995,7 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68: dependencies: '@babel/runtime': 7.29.2 '@matrix-org/matrix-sdk-crypto-wasm': 18.2.0 @@ -13060,6 +13072,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.0: {} + set-blocking@2.0.0: {} set-cookie-parser@2.7.2: {} From 78417ff2cb37baad149d7b762fdac7b3b29696df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 09:05:59 +0200 Subject: [PATCH 010/100] Update dependency @livekit/components-react to v2.9.21 (#3957) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3ad458e..544da4cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,7 +50,7 @@ importers: version: 0.12.13(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) '@livekit/components-react': specifier: ^2.0.0 - version: 2.9.20(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1) + version: 2.9.21(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1) '@livekit/protocol': specifier: ^1.42.2 version: 1.45.6 @@ -1556,12 +1556,12 @@ packages: livekit-client: ^2.17.2 tslib: ^2.6.2 - '@livekit/components-react@2.9.20': - resolution: {integrity: sha512-hjkYOsJj9Jbghb7wM5cI8HoVisKeL6Zcy1VnRWTLm0sqVbto8GJp/17T4Udx85mCPY6Jgh8I1Cv0yVzgz7CQtg==} + '@livekit/components-react@2.9.21': + resolution: {integrity: sha512-6hU9VucJJL+gAhilNGe4MBCDCZVk64qyjP9Ck86krvOIdVU76WeWksddg1MYUP10AlUwwrfD7davz41pJTcMJw==} engines: {node: '>=18'} peerDependencies: '@livekit/krisp-noise-filter': ^0.2.12 || ^0.3.0 - livekit-client: ^2.17.2 + livekit-client: ^2.18.2 react: '>=18' react-dom: '>=18' tslib: ^2.6.2 @@ -4842,9 +4842,6 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@6.2.2: - resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} - jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} @@ -8222,12 +8219,12 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - '@livekit/components-react@2.9.20(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)': + '@livekit/components-react@2.9.21(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)': dependencies: '@livekit/components-core': 0.12.13(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) clsx: 2.1.1 events: 3.3.0 - jose: 6.2.2 + jose: 6.2.3 livekit-client: 2.18.9(@types/dom-mediacapture-record@1.0.22) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -11748,8 +11745,6 @@ snapshots: jiti@2.6.1: {} - jose@6.2.2: {} - jose@6.2.3: {} js-tokens@10.0.0: {} From f4ff790d2caa4638078416c9e0a37fc7653458e6 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Mon, 11 May 2026 17:50:33 +0800 Subject: [PATCH 011/100] Add `MediaMuteAndSwitchButton` component (storybook only) (#3938) * add MeidaMuteAndSwitchButton * User button in footer * Add tests * update styling (dark bg on menu open + chevron white + chevron up) * fix tests * add storybook to CI only add storybook with storybook label test names another env name test TestName new default name remove label condition Update pr-deploy.yaml * Update pr-deploy.yaml * add toggle example to default component * hook up footer select actions * fix video audio button (swapped) and lable in story --- src/components/CallFooter.stories.tsx | 18 ++ src/components/CallFooter.tsx | 88 +++++-- .../MediaMuteAndSwitchButton.module.css | 37 +++ .../MediaMuteAndSwitchButton.stories.tsx | 117 +++++++++ .../MediaMuteAndSwitchButton.test.tsx | 226 +++++++++++++++++ src/components/MediaMuteAndSwitchButton.tsx | 230 ++++++++++++++++++ .../MediaMuteAndSwitchButton.test.tsx.snap | 43 ++++ 7 files changed, 741 insertions(+), 18 deletions(-) create mode 100644 src/components/MediaMuteAndSwitchButton.module.css create mode 100644 src/components/MediaMuteAndSwitchButton.stories.tsx create mode 100644 src/components/MediaMuteAndSwitchButton.test.tsx create mode 100644 src/components/MediaMuteAndSwitchButton.tsx create mode 100644 src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap diff --git a/src/components/CallFooter.stories.tsx b/src/components/CallFooter.stories.tsx index 20c7c4c0..7090a338 100644 --- a/src/components/CallFooter.stories.tsx +++ b/src/components/CallFooter.stories.tsx @@ -88,6 +88,24 @@ export const Default: Story = { }, }; +export const WithAudioAndVideoOptions: Story = { + ...Default, + args: { + ...Default.args, + audioEnabled: false, + videoEnabled: true, + audioOptions: [ + { label: "Microphone 1", id: "1" }, + { label: "Microphone 2", id: "2" }, + ], + videoOptions: [ + { label: "Camera 1", id: "1" }, + { label: "Camera 2", id: "2" }, + ], + selectedAudio: "2", + selectedVideo: "1", + }, +}; export const WithLogo: Story = { ...Default, args: { diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index 9d59d2d1..afc5bdc9 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -30,6 +30,10 @@ import { } from "../button"; import styles from "./CallFooter.module.css"; import { type GridMode } from "../state/CallViewModel/CallViewModel"; +import { + MediaMuteAndSwitchButton, + type MenuOptions, +} from "./MediaMuteAndSwitchButton"; export interface AudioOutputSwitcher { targetOutput: string; @@ -79,6 +83,13 @@ export interface FooterProps { // debug stuff debugTileLayout?: boolean; tileStoreGeneration?: number; + + audioOptions?: MenuOptions[]; + videoOptions?: MenuOptions[]; + selectedAudio?: string; + selectedVideo?: string; + selectAudioDevice?: (deviceId: string) => void; + selectVideoDevice?: (deviceId: string) => void; } export const CallFooter: FC = ({ @@ -104,6 +115,13 @@ export const CallFooter: FC = ({ hangup, debugTileLayout, tileStoreGeneration, + + audioOptions, + videoOptions, + selectedAudio, + selectedVideo, + selectAudioDevice, + selectVideoDevice, }) => { const buttons: JSX.Element[] = []; const buttonSize = asPip ? "md" : "lg"; @@ -125,24 +143,58 @@ export const CallFooter: FC = ({ ); } - buttons.push( - , - , - ); + if ((audioOptions?.length ?? 0) > 0) { + buttons.push( + , + ); + } else { + buttons.push( + , + ); + } + if ((videoOptions?.length ?? 0) > 0) { + buttons.push( + , + ); + } else { + buttons.push( + , + ); + } if (toggleScreenSharing !== undefined) { buttons.push( diff --git a/src/components/MediaMuteAndSwitchButton.module.css b/src/components/MediaMuteAndSwitchButton.module.css new file mode 100644 index 00000000..e5bba238 --- /dev/null +++ b/src/components/MediaMuteAndSwitchButton.module.css @@ -0,0 +1,37 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +.container { + display: flex; + flex-direction: row; + align-items: center; + background-color: var(--cpd-color-bg-subtle-secondary); + border-radius: 32px; + transition: background-color 0.2s ease-in-out; +} +.containerOpen { + background-color: var(--cpd-color-bg-action-primary-pressed); +} +.chevronIconOpen > svg { + color: var(--cpd-color-icon-on-solid-primary); +} +.menuButton { + width: 40px; + background-color: transparent !important; +} +.itemIcon { + color: var(--cpd-color-text-secondary); +} + +.rotate { + animation: spinner 1.5s linear infinite; +} +@keyframes spinner { + to { + transform: rotate(360deg); + } +} diff --git a/src/components/MediaMuteAndSwitchButton.stories.tsx b/src/components/MediaMuteAndSwitchButton.stories.tsx new file mode 100644 index 00000000..bbf9f159 --- /dev/null +++ b/src/components/MediaMuteAndSwitchButton.stories.tsx @@ -0,0 +1,117 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { AdvancedSettingsIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { fn, userEvent, within, expect } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton"; + +const meta = { + component: MediaMuteAndSwitchButton, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: "SomeMenu", + iconsAndLabels: { + IconEnabled: AdvancedSettingsIcon, + IconDisabled: AdvancedSettingsIcon, + enabledLabel: "Enabled", + disabledLabel: "Disabled", + optionsButtonLabel: "Options", + }, + enabled: true, + options: [ + { label: "option 1", id: "1" }, + { label: "option 2", id: "2" }, + ], + selectedOption: "1", + onMuteClick: fn(), + onSelect: fn(), + }, +}; + +export const AudioMute: Story = { + args: { + ...Default.args, + title: "Microphone", + iconsAndLabels: "audio", + enabled: false, + options: [ + { label: "Microphone 1", id: "1" }, + { label: "Microphone 2", id: "2" }, + ], + toggles: [ + { + label: "example toggle", + id: "t0", + enabled: true, + }, + ], + selectedOption: "2", + }, + play: async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + // Both the mute button and the chevron trigger currently share the aria-label "Edit" + // (both are TODO placeholders in the component). The mute button is first in the DOM. + const muteButton = canvas.getByLabelText("Unmute microphone"); + await userEvent.click(muteButton); + await expect(args.onMuteClick).toHaveBeenCalled(); + }, +}; + +export const AudioUnmute: Story = { + args: { + title: "Microphone", + iconsAndLabels: "audio", + enabled: true, + options: [ + { label: "Microphone 1", id: "1" }, + { label: "Microphone 2", id: "2" }, + ], + toggles: [], + selectedOption: "2", + }, +}; + +export const VideoMute: Story = { + args: { + title: "Camera", + iconsAndLabels: "video", + enabled: false, + options: [ + { label: "Camera 1", id: "1" }, + { label: "Camera 2", id: "2" }, + ], + toggles: [], + selectedOption: "1", + }, +}; + +export const VideoUnmute: Story = { + args: { + title: "Camera", + iconsAndLabels: "video", + enabled: true, + options: [ + { label: "Camera 1", id: "1" }, + { label: "Camera 2", id: "2" }, + ], + toggles: [ + { + label: "Blur Background", + id: "background_blurring", + enabled: false, + }, + ], + selectedOption: "2", + }, +}; diff --git a/src/components/MediaMuteAndSwitchButton.test.tsx b/src/components/MediaMuteAndSwitchButton.test.tsx new file mode 100644 index 00000000..42a8d970 --- /dev/null +++ b/src/components/MediaMuteAndSwitchButton.test.tsx @@ -0,0 +1,226 @@ +/* +Copyright 2023, 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { describe, expect, test, vi } from "vitest"; +import { act, render, screen, type RenderResult } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { type JSX, useState } from "react"; + +import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton"; + +describe("MediaMuteAndSwitchButton", () => { + test("renders", () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + test("renders correct audio and video labels", () => { + const renderLabels = ( + type: "video" | "audio", + enabled: boolean, + ): RenderResult => { + return render( + , + ); + }; + const renderAudioEndabled = renderLabels("audio", true); + const renderAudioDisabled = renderLabels("audio", false); + const renderVideoEnabled = renderLabels("video", true); + const renderVideoDisabled = renderLabels("video", false); + + expect( + renderAudioEndabled.getByRole("button", { name: "Mute microphone" }), + ).toBeInTheDocument(); + expect( + renderAudioDisabled.getByRole("button", { name: "Unmute microphone" }), + ).toBeInTheDocument(); + expect( + renderVideoEnabled.getByRole("button", { name: "Start video" }), + ).toBeInTheDocument(); + expect( + renderVideoDisabled.getByRole("button", { name: "Stop video" }), + ).toBeInTheDocument(); + }); + + test("calls mute on mute press", async () => { + const user = userEvent.setup(); + const onMute = vi.fn(); + const { getByRole } = render( + , + ); + + await user.click(getByRole("button", { name: "Mute microphone" })); + + expect(onMute).toHaveBeenCalled(); + }); + + test("calls select callback on menu click", async () => { + const user = userEvent.setup(); + const onSelect = vi.fn(); + const { getByRole } = render( + , + ); + + await user.click(getByRole("button", { name: "Microphone" })); + await user.click(screen.getByRole("menuitem", { name: "Microphone 2" })); + + expect(onSelect).toHaveBeenCalledWith("mic2"); + }); + test("does not call select callback on already selected menu click", async () => { + const user = userEvent.setup(); + const onSelect = vi.fn(); + const { getByRole } = render( + , + ); + + await user.click(getByRole("button", { name: "Microphone" })); + await user.click(screen.getByRole("menuitem", { name: "Microphone 1" })); + + expect(onSelect).not.toHaveBeenCalled(); + }); + + test("renders menu spinner until selection updates for the component", async () => { + const user = userEvent.setup(); + const { promise, resolve } = Promise.withResolvers(); + const onSelectPressed = vi.fn(); + const onOptionUpdated = vi.fn(); + function Wrapper(): JSX.Element { + const [selectedOption, setSelectedOption] = useState("mic1"); + return ( + { + onSelectPressed(); + void promise.then(() => { + setSelectedOption(id); + onOptionUpdated(); + }); + }} + /> + ); + } + + const { getByRole } = render(); + + await user.click(getByRole("button", { name: "Microphone" })); + await user.click(screen.getByRole("menuitem", { name: "Microphone 2" })); + + expect(onSelectPressed).toHaveBeenCalled(); + expect(onOptionUpdated).not.toHaveBeenCalled(); + // After clicking, plannedSelection="mic2" but selectedOption is still "mic1", + // so a spinner should appear on the mic2 item + const mic2Item = screen.getByRole("menuitem", { name: "Microphone 2" }); + expect(mic2Item.querySelector(".rotate")).toBeTruthy(); + + // The currently-selected mic1 item should not have a spinner + const mic1Item = screen.getByRole("menuitem", { name: "Microphone 1" }); + expect(mic1Item.querySelector(".rotate")).toBeNull(); + await act(async () => { + // resolve the promise that acutally updates the select option. + resolve(); + await promise; + }); + + expect(onOptionUpdated).toHaveBeenCalled(); + // Spinner should now be gone since the selection has caught up + const mic2ItemAfter = screen.getByRole("menuitem", { + name: "Microphone 2", + }); + expect(mic2ItemAfter.querySelector(".rotate")).toBeNull(); + }); + + test("renders menu with toggle control and calls toggle callback", async () => { + const user = userEvent.setup(); + const onSelect = vi.fn(); + const { getByRole } = render( + , + ); + + await user.click(getByRole("button", { name: "Microphone" })); + + const toggle = screen.getByRole("menuitemcheckbox", { + name: "Background blur", + }); + expect(toggle).toBeInTheDocument(); + expect(toggle).toHaveAttribute("aria-checked", "false"); + + await user.click(toggle); + + expect(onSelect).toHaveBeenCalledWith("bg_blur"); + }); + + test("renders check icon to mark the selected menu item", async () => { + const user = userEvent.setup(); + const { getByRole } = render( + , + ); + + // open menu + await user.click(getByRole("button", { name: "Microphone" })); + + // The selected item (mic2) renders both an IconOptions SVG and a CheckIcon SVG + const mic1Item = screen.getByRole("menuitem", { name: "Microphone 2" }); + expect(mic1Item.querySelectorAll("svg").length).toBe(2); + + // The unselected item (mic1) only renders its IconOptions SVG + const mic2Item = screen.getByRole("menuitem", { name: "Microphone 1" }); + expect(mic2Item.querySelectorAll("svg").length).toBe(1); + }); +}); diff --git a/src/components/MediaMuteAndSwitchButton.tsx b/src/components/MediaMuteAndSwitchButton.tsx new file mode 100644 index 00000000..7e38c7c6 --- /dev/null +++ b/src/components/MediaMuteAndSwitchButton.tsx @@ -0,0 +1,230 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { type ComponentType, useState, type FC } from "react"; +import { + Button, + Menu, + MenuItem, + ToggleMenuItem, +} from "@vector-im/compound-web"; +import { t } from "i18next"; +import { + CheckIcon, + ChevronUpIcon, + ChevronDownIcon, + MicOffSolidIcon, + MicOnIcon, + MicOnSolidIcon, + SpinnerIcon, + VideoCallIcon, + VideoCallOffSolidIcon, + VideoCallSolidIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; +import classNames from "classnames"; +import { logger } from "matrix-js-sdk/lib/logger"; + +import styles from "./MediaMuteAndSwitchButton.module.css"; + +export interface MenuOptions { + label: string; + id: string; +} +export interface ToggleOption { + label: string; + enabled: boolean; + id: string; +} + +export interface IconsAndLabels { + /** The Icon used if the mute button is enabled */ + IconEnabled: ComponentType>; + /** The Icon used if the mute button is disabled */ + IconDisabled: ComponentType>; + /** The icon used for the different options */ + IconOptions?: ComponentType>; + enabledLabel: string; + disabledLabel: string; + optionsButtonLabel: string; +} + +export interface MediaMuteAndSwitchButtonProps { + /** The title used in the Switcher modal. */ + title: string; + /** If the Mute button is enabled */ + enabled?: boolean; + /** Callback if the mute button is clicked */ + onMuteClick?: () => void; + iconsAndLabels?: "video" | "audio" | IconsAndLabels; + /** The options available for the media device selector modal */ + options?: MenuOptions[]; + /** The option that will currently be rendered as the selected option */ + selectedOption?: string; + /** + * The available toggles (including there current state) + * The toggle state is not stored by this component. + * It is handled externally and needs to be set by listening to the `onSelect` callback and setting the right toggle item to `enabled` + */ + toggles?: ToggleOption[]; + /** + * For any toggle and option this method will be called. + * So toggles need to be implemented by listening here and setting the right toggle item to `enabled` + */ + onSelect?: (id: string) => void; +} + +export const MediaMuteAndSwitchButton: FC = ({ + title, + enabled, + onMuteClick, + iconsAndLabels: iconsAndLabelsWithDefaultCases, + options, + selectedOption, + toggles, + onSelect, +}) => { + const [plannedSelection, setPlannedSelection] = useState(null); + const [menuOpen, setMenuOpen] = useState(false); + let iconsAndLabels: IconsAndLabels | undefined; + switch (iconsAndLabelsWithDefaultCases) { + case "video": + iconsAndLabels = { + IconEnabled: VideoCallSolidIcon, + IconDisabled: VideoCallOffSolidIcon, + IconOptions: VideoCallIcon, + disabledLabel: t("stop_video_button_label"), + enabledLabel: t("start_video_button_label"), + optionsButtonLabel: t("settings.devices.microphone"), + }; + break; + case "audio": + iconsAndLabels = { + IconEnabled: MicOnSolidIcon, + IconDisabled: MicOffSolidIcon, + IconOptions: MicOnIcon, + disabledLabel: t("mute_microphone_button_label"), + enabledLabel: t("unmute_microphone_button_label"), + optionsButtonLabel: t("settings.devices.microphone"), + }; + break; + default: + iconsAndLabels = iconsAndLabelsWithDefaultCases; + break; + } + const { + IconEnabled, + IconDisabled, + IconOptions, + disabledLabel, + enabledLabel, + optionsButtonLabel, + } = iconsAndLabels ?? { + IconEnabled: undefined, + IconDisabled: undefined, + IconOptions: undefined, + disabledLabel: undefined, + enabledLabel: undefined, + optionsButtonLabel: undefined, + }; + { + logger.info( + "RENDER WITH: selectedOption !== option.id && plannedSelection === option.id", + selectedOption, + " !==", + "option.id", + " && ", + plannedSelection, + " === ", + "option.id", + ); + } + return ( +
+ {/* The mute button lives inside */} +
+ ); +}; diff --git a/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap b/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap new file mode 100644 index 00000000..84ea220a --- /dev/null +++ b/src/components/__snapshots__/MediaMuteAndSwitchButton.test.tsx.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`MediaMuteAndSwitchButton > renders 1`] = ` +
+
+ +
+
+`; From 20d14fa4e0923eeebabb0143fc2531dac26eec80 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 16:39:53 +0200 Subject: [PATCH 012/100] use vite storybook plugin for storybook tests. run via pnpm test:storybook --- .storybook/main.ts | 2 +- README.md | 2 +- package.json | 16 ++- pnpm-lock.yaml | 271 +++++++++++++++++++++++++++++++++++---------- vite.config.ts | 30 +++-- vitest.config.ts | 52 +++++++-- 6 files changed, 285 insertions(+), 88 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 977eca73..9a3f0b53 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -9,7 +9,7 @@ import type { StorybookConfig } from "@storybook/react-vite"; const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], - addons: ["@storybook/addon-docs"], + addons: ["@storybook/addon-docs", "@storybook/addon-vitest"], framework: "@storybook/react-vite", }; export default config; diff --git a/README.md b/README.md index 0c82e4b0..167fe675 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ including federation: - Minimum TLS reverse proxy for - Synapse homeserver: `synapse.m.localhost` and `synapse.othersite.m.localhost` - MatrixRTC backend: `matrix-rtc.m.localhost` and `matrix-rtc.othersite.m.localhost` - - Local Element Call development `call.m.localhost` via `yarn dev --host ` + - Local Element Call development `call.m.localhost` via `pnpm dev --host ` - Element Web `app.m.localhost` and `app.othersite.m.localhost` - Note certificates will expire on Thr, 20 September 2035 14:27:35 CEST diff --git a/package.json b/package.json index fef415eb..336b3e66 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "i18n": "i18next", "i18n:check": "i18next --fail-on-warnings --fail-on-update", "test": "vitest", + "test:storybook": "vitest --project=storybook", + "test:unit": "vitest --project=unit", "test:coverage": "vitest --coverage", "backend": "docker-compose -f dev-backend-docker-compose.yml up", "backend-playwright": "docker-compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml up", @@ -59,8 +61,9 @@ "@react-spring/web": "^10.0.0", "@sentry/react": "^8.0.0", "@sentry/vite-plugin": "^3.0.0", - "@storybook/addon-docs": "^10.3.3", - "@storybook/react-vite": "^10.3.3", + "@storybook/addon-docs": "^10.3.6", + "@storybook/addon-vitest": "^10.3.6", + "@storybook/react-vite": "^10.3.6", "@stylistic/eslint-plugin": "^3.0.0", "@testing-library/dom": "^10.1.0", "@testing-library/jest-dom": "^6.6.3", @@ -83,7 +86,9 @@ "@vector-im/compound-design-tokens": "^10.0.0", "@vector-im/compound-web": "^9.3.0", "@vitejs/plugin-react": "^4.0.1", + "@vitest/browser-playwright": "^4.1.5", "@vitest/coverage-v8": "^4.0.18", + "@vitest/ui": "4.1.5", "babel-plugin-transform-vite-meta-env": "^1.0.3", "classnames": "^2.3.1", "copy-to-clipboard": "^3.3.3", @@ -98,7 +103,7 @@ "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-rxjs": "^5.0.3", - "eslint-plugin-storybook": "^10.3.3", + "eslint-plugin-storybook": "^10.3.6", "eslint-plugin-unicorn": "^56.0.0", "fetch-mock": "11.1.5", "global-jsdom": "^26.0.0", @@ -116,6 +121,7 @@ "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", "pako": "^2.0.4", + "playwright": "^1.59.1", "postcss": "^8.4.41", "postcss-preset-env": "^10.0.0", "posthog-js": "1.160.3", @@ -128,7 +134,7 @@ "react-use-measure": "^2.1.1", "rxjs": "^7.8.1", "sass": "^1.42.1", - "storybook": "^10.3.3", + "storybook": "^10.3.6", "typescript": "^5.8.3", "typescript-eslint-language-service": "^5.0.5", "unique-names-generator": "^4.6.0", @@ -141,7 +147,7 @@ "vite-plugin-node-stdlib-browser": "^0.2.1", "vite-plugin-svgr": "^4.0.0", "vite-plugin-wasm": "^3.6.0", - "vitest": "^4.0.18", + "vitest": "^4.1.5", "vitest-axe": "^1.0.0-pre.3" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 544da4cb..ebb0171a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,11 +82,14 @@ importers: specifier: ^3.0.0 version: 3.6.1 '@storybook/addon-docs': - specifier: ^10.3.3 - version: 10.3.5(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + specifier: ^10.3.6 + version: 10.3.6(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + '@storybook/addon-vitest': + specifier: ^10.3.6 + version: 10.3.6(@vitest/browser-playwright@4.1.5)(@vitest/browser@4.1.5)(@vitest/runner@4.1.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vitest@4.1.5) '@storybook/react-vite': - specifier: ^10.3.3 - version: 10.3.5(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + specifier: ^10.3.6 + version: 10.3.6(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) '@stylistic/eslint-plugin': specifier: ^3.0.0 version: 3.1.0(eslint@8.57.1)(typescript@5.9.3) @@ -153,8 +156,14 @@ importers: '@vitejs/plugin-react': specifier: ^4.0.1 version: 4.7.0(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/browser-playwright': + specifier: ^4.1.5 + version: 4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5) '@vitest/coverage-v8': specifier: ^4.0.18 + version: 4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5) + '@vitest/ui': + specifier: 4.1.5 version: 4.1.5(vitest@4.1.5) babel-plugin-transform-vite-meta-env: specifier: ^1.0.3 @@ -199,8 +208,8 @@ importers: specifier: ^5.0.3 version: 5.0.3(eslint@8.57.1)(typescript@5.9.3) eslint-plugin-storybook: - specifier: ^10.3.3 - version: 10.3.5(eslint@8.57.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + specifier: ^10.3.6 + version: 10.3.6(eslint@8.57.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) eslint-plugin-unicorn: specifier: ^56.0.0 version: 56.0.1(eslint@8.57.1) @@ -252,6 +261,9 @@ importers: pako: specifier: ^2.0.4 version: 2.1.0 + playwright: + specifier: ^1.59.1 + version: 1.59.1 postcss: specifier: ^8.4.41 version: 8.5.11 @@ -289,8 +301,8 @@ importers: specifier: ^1.42.1 version: 1.99.0 storybook: - specifier: ^10.3.3 - version: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^10.3.6 + version: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) typescript: specifier: ^5.8.3 version: 5.9.3 @@ -328,8 +340,8 @@ importers: specifier: ^3.6.0 version: 3.6.0(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) vitest: - specifier: ^4.0.18 - version: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + specifier: ^4.1.5 + version: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) vitest-axe: specifier: ^1.0.0-pre.3 version: 1.0.0-pre.5(vitest@4.1.5) @@ -941,6 +953,9 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + '@bufbuild/protobuf@1.10.1': resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==} @@ -1874,6 +1889,9 @@ packages: engines: {node: '>=18'} hasBin: true + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2679,23 +2697,41 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-docs@10.3.5': - resolution: {integrity: sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==} + '@storybook/addon-docs@10.3.6': + resolution: {integrity: sha512-TvIdADVPtauxW0LzXIpIv7X6GxwetorhyNh+6+7MHC27XSBCWVxxRUwL63YeLlHTuXsIk0quG3b1xgwVRzWOJA==} peerDependencies: - storybook: ^10.3.5 + storybook: ^10.3.6 - '@storybook/builder-vite@10.3.5': - resolution: {integrity: sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==} + '@storybook/addon-vitest@10.3.6': + resolution: {integrity: sha512-HXj7RrPJY+xzoNjL+xZu2oLw1fI5BA87Noh1NAXMPuECHR5R5fuRM/tTsJuIGXHFMO06FjSi/rekDIfCj1fL4w==} peerDependencies: - storybook: ^10.3.5 + '@vitest/browser': ^3.0.0 || ^4.0.0 + '@vitest/browser-playwright': ^4.0.0 + '@vitest/runner': ^3.0.0 || ^4.0.0 + storybook: ^10.3.6 + vitest: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/runner': + optional: true + vitest: + optional: true + + '@storybook/builder-vite@10.3.6': + resolution: {integrity: sha512-gpvR/sE4BcrFtmQZ+Ker7zD23oQzoVeqD9nF6cK6yzY+Q0svJXyX2EPmFG4y+EwygD5/vNzDpP84gGMut8VRwg==} + peerDependencies: + storybook: ^10.3.6 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/csf-plugin@10.3.5': - resolution: {integrity: sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==} + '@storybook/csf-plugin@10.3.6': + resolution: {integrity: sha512-9kBf7VRdRqTSIYo+rPtVn5yjYYyK8kP2QhEYx3oiXvfwy4RexmbJnhk/tXa/lNiTqukA1TqaWQ2+5MqF4fu6YQ==} peerDependencies: esbuild: ^0.28.0 rollup: '*' - storybook: ^10.3.5 + storybook: ^10.3.6 vite: '*' webpack: '*' peerDependenciesMeta: @@ -2717,27 +2753,27 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/react-dom-shim@10.3.5': - resolution: {integrity: sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==} + '@storybook/react-dom-shim@10.3.6': + resolution: {integrity: sha512-/Tu1gPu+Fw+zOnAGmxRmOD30FX3a04LxcTAKflEtdpmtIMVR5bA3qpjy+f5YhoyDCecbXyKmL1OeIU2FIIZHqQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.5 + storybook: ^10.3.6 - '@storybook/react-vite@10.3.5': - resolution: {integrity: sha512-UB5sJHeh26bfd8sNMx2YPGYRYmErIdTRaLOT28m4bykQIa1l9IgVktsYg/geW7KsJU0lXd3oTbnUjLD+enpi3w==} + '@storybook/react-vite@10.3.6': + resolution: {integrity: sha512-tySQRc+8q7V2NkylQMNJjDV8zXy6tkxb8oDqw/DIhHhI9Xn77MTKVZ8Cihbo5NMm7HYTB6xDKr6wqdSMgdufYQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.5 + storybook: ^10.3.6 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react@10.3.5': - resolution: {integrity: sha512-tpLTLaVGoA6fLK3ReyGzZUricq7lyPaV2hLPpj5wqdXLV/LpRtAHClUpNoPDYSBjlnSjL81hMZijbkGC3mA+gw==} + '@storybook/react@10.3.6': + resolution: {integrity: sha512-oZQZ6xayWe5IdHmFUTL0TL8rX/gpNNh9gWhT2vzW5eeUvlkVG/RBKdsja6Ndrk2s1D9vcnwiI6r6CNXy3IEEmg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.5 + storybook: ^10.3.6 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -3173,6 +3209,17 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/browser-playwright@4.1.5': + resolution: {integrity: sha512-CWy0lBQJq97nionyJJdnaU4961IXTl43a7UCu5nHy51IoKxAt6PVIJLo+76rVl7KOOgcWHNkG4kbJu/pW7knvA==} + peerDependencies: + playwright: '*' + vitest: 4.1.5 + + '@vitest/browser@4.1.5': + resolution: {integrity: sha512-iCDGI8c4yg+xmjUg2VsygdAUSIIB4x5Rht/P68OXy1hPELKXHDkzh87lkuTcdYmemRChDkEpB426MmDjzC0ziA==} + peerDependencies: + vitest: 4.1.5 + '@vitest/coverage-v8@4.1.5': resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==} peerDependencies: @@ -3217,6 +3264,11 @@ packages: '@vitest/spy@4.1.5': resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + '@vitest/ui@4.1.5': + resolution: {integrity: sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==} + peerDependencies: + vitest: 4.1.5 + '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} @@ -4165,11 +4217,11 @@ packages: eslint: ^8.0.0 typescript: '>=4.0.0' - eslint-plugin-storybook@10.3.5: - resolution: {integrity: sha512-rEFkfU3ypF44GpB4tiJ9EFDItueoGvGi3+weLHZax2ON2MB7VIDsxdSUGvIU5tMURg+oWYlpzCyLm4TpDq2deA==} + eslint-plugin-storybook@10.3.6: + resolution: {integrity: sha512-8udrL+Rmp5LFaZvgRe4J226X1MYls25bWCyHuzR5X8s2qbFTryX+wKC+o/0Ato4A1AvwnDg8OOMPc6yWJ9JpcA==} peerDependencies: eslint: '>=8' - storybook: ^10.3.5 + storybook: ^10.3.6 eslint-plugin-unicorn@56.0.1: resolution: {integrity: sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==} @@ -4311,6 +4363,9 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -5154,6 +5209,10 @@ packages: resolution: {integrity: sha512-Q9wJ/xhzeD9Wua1MwDN2v3ah3HENsUVSlzzL9Qw149cL9hHZkXtQGl3Eq36BbdLV+/qUwaP1WtJQ+H/+Oxso8g==} engines: {node: 20 || 22 || 24} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -5429,6 +5488,10 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -6059,6 +6122,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -6116,14 +6183,17 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - storybook@10.3.5: - resolution: {integrity: sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==} + storybook@10.3.6: + resolution: {integrity: sha512-vbSz7g/1rGMC1uAULqMZjALkIuLu2QABqfhRYhyr/11kzyesi+vAmwyJLukZP1FfecxGOgMwOh6GS0YsGpHAvQ==} hasBin: true peerDependencies: prettier: ^2 || ^3 + vite-plus: ^0.1.15 peerDependenciesMeta: prettier: optional: true + vite-plus: + optional: true stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -6295,6 +6365,10 @@ packages: toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -7634,6 +7708,8 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@blazediff/core@1.9.1': {} + '@bufbuild/protobuf@1.10.1': {} '@codecov/bundler-plugin-core@1.9.1': @@ -8473,6 +8549,8 @@ snapshots: dependencies: playwright: 1.59.1 + '@polka/url@1.0.0-next.29': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -9139,15 +9217,15 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': + '@storybook/addon-docs@10.3.6(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5) - '@storybook/csf-plugin': 10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + '@storybook/csf-plugin': 10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + '@storybook/react-dom-shim': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -9156,10 +9234,24 @@ snapshots: - vite - webpack - '@storybook/builder-vite@10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': + '@storybook/addon-vitest@10.3.6(@vitest/browser-playwright@4.1.5)(@vitest/browser@4.1.5)(@vitest/runner@4.1.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vitest@4.1.5)': dependencies: - '@storybook/csf-plugin': 10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) - storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@storybook/global': 5.0.0 + '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + optionalDependencies: + '@vitest/browser': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5) + '@vitest/browser-playwright': 4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5) + '@vitest/runner': 4.1.5 + vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - react + - react-dom + + '@storybook/builder-vite@10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': + dependencies: + '@storybook/csf-plugin': 10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 vite: 8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) transitivePeerDependencies: @@ -9167,9 +9259,9 @@ snapshots: - rollup - webpack - '@storybook/csf-plugin@10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': + '@storybook/csf-plugin@10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': dependencies: - storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) unplugin: 2.3.11 optionalDependencies: esbuild: 0.28.0 @@ -9183,25 +9275,25 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@storybook/react-dom-shim@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': + '@storybook/react-dom-shim@10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@storybook/react-vite@10.3.5(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': + '@storybook/react-vite@10.3.6(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - '@storybook/builder-vite': 10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) - '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + '@storybook/builder-vite': 10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + '@storybook/react': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 react: 19.2.5 react-docgen: 8.0.3 react-dom: 19.2.5(react@19.2.5) resolve: 1.22.12 - storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tsconfig-paths: 4.2.0 vite: 8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) transitivePeerDependencies: @@ -9211,15 +9303,15 @@ snapshots: - typescript - webpack - '@storybook/react@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)': + '@storybook/react@10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + '@storybook/react-dom-shim': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) react: 19.2.5 react-docgen: 8.0.3 react-docgen-typescript: 2.4.0(typescript@5.9.3) react-dom: 19.2.5(react@19.2.5) - storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -9745,7 +9837,37 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.1.5(vitest@4.1.5)': + '@vitest/browser-playwright@4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)': + dependencies: + '@vitest/browser': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5) + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + playwright: 1.59.1 + tinyrainbow: 3.1.0 + vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/utils': 4.1.5 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/coverage-v8@4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.5 @@ -9757,7 +9879,9 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + optionalDependencies: + '@vitest/browser': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5) '@vitest/expect@3.2.4': dependencies: @@ -9810,6 +9934,17 @@ snapshots: '@vitest/spy@4.1.5': {} + '@vitest/ui@4.1.5(vitest@4.1.5)': + dependencies: + '@vitest/utils': 4.1.5 + fflate: 0.8.2 + flatted: 3.4.2 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -10966,11 +11101,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-storybook@10.3.5(eslint@8.57.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3): + eslint-plugin-storybook@10.3.6(eslint@8.57.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.58.2(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 - storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) transitivePeerDependencies: - supports-color - typescript @@ -11151,6 +11286,8 @@ snapshots: fflate@0.4.8: {} + fflate@0.8.2: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -12051,6 +12188,8 @@ snapshots: mktemp@2.0.2: {} + mrmime@2.0.1: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -12381,6 +12520,8 @@ snapshots: pngjs@5.0.0: {} + pngjs@7.0.0: {} + possible-typed-array-names@1.1.0: {} postcss-attribute-case-insensitive@7.0.1(postcss@8.5.11): @@ -13141,6 +13282,12 @@ snapshots: signal-exit@4.1.0: {} + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -13195,7 +13342,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -13429,6 +13576,8 @@ snapshots: toggle-selection@1.0.6: {} + totalist@3.0.1: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -13788,9 +13937,9 @@ snapshots: axe-core: 4.11.3 chalk: 5.6.2 lodash-es: 4.18.1 - vitest: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) - vitest@4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + vitest@4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.5 '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -13814,7 +13963,9 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.12.2 - '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) + '@vitest/browser-playwright': 4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5) + '@vitest/coverage-v8': 4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5) + '@vitest/ui': 4.1.5(vitest@4.1.5) jsdom: 26.1.0 transitivePeerDependencies: - msw diff --git a/vite.config.ts b/vite.config.ts index 801ea79a..59781d3a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -24,18 +24,10 @@ import react from "@vitejs/plugin-react"; import { realpathSync } from "fs"; import * as fs from "node:fs"; -// https://vitejs.dev/config/ -// Modified type helper from defineConfig to allow for packageType (see defineConfig from vite) -export default ({ +export const vitePluginsConfig = ({ mode, - packageType, -}: ConfigEnv & { packageType?: "full" | "embedded" }): UserConfig => { +}: Pick): UserConfig => { const env = loadEnv(mode, process.cwd()); - // Environment variables with the VITE_ prefix are accessible at runtime. - // So, we set this to allow for build/package specific behavior. - // In future we might be able to do what is needed via code splitting at - // build time. - process.env.VITE_PACKAGE = packageType ?? "full"; const plugins: PluginOption[] = [ react(), wasm(), @@ -72,7 +64,7 @@ export default ({ ); } - if (!process.env.STORYBOOK) { + if (!process.env.STORYBOOK && !process.env.VITEST) { plugins.push( createHtmlPlugin({ entry: "src/main.tsx", @@ -86,6 +78,20 @@ export default ({ ); } + return { plugins }; +}; +// https://vitejs.dev/config/ +// Modified type helper from defineConfig to allow for packageType (see defineConfig from vite) +export default ({ + mode, + packageType, +}: ConfigEnv & { packageType?: "full" | "embedded" }): UserConfig => { + // Environment variables with the VITE_ prefix are accessible at runtime. + // So, we set this to allow for build/package specific behavior. + // In future we might be able to do what is needed via code splitting at + // build time. + process.env.VITE_PACKAGE = packageType ?? "full"; + // The crypto WASM module is imported dynamically. Since it's common // for developers to use a linked copy of matrix-js-sdk or Rust // crypto (which could reside anywhere on their file system), Vite @@ -102,6 +108,7 @@ export default ({ console.log("Allowed vite paths:", allow); return { + ...vitePluginsConfig({ mode }), server: { port: 3000, fs: { allow }, @@ -136,7 +143,6 @@ export default ({ }, }, }, - plugins, resolve: { alias: { // matrix-widget-api has its transpiled lib/index.js as its entry point, diff --git a/vitest.config.ts b/vitest.config.ts index 90082f58..40fd4288 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,20 +1,54 @@ import { defineConfig, mergeConfig } from "vitest/config"; +import { playwright } from "@vitest/browser-playwright"; +import { vitePluginsConfig } from "./vite.config"; +import { storybookTest } from "@storybook/addon-vitest/vitest-plugin"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; -import viteConfig from "./vite.config"; +const dirname = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig((configEnv) => mergeConfig( - viteConfig(configEnv), + vitePluginsConfig(configEnv), defineConfig({ test: { - environment: "jsdom", - css: { - modules: { - classNameStrategy: "non-scoped", + projects: [ + { + extends: true, + test: { + css: { + modules: { + classNameStrategy: "non-scoped", + }, + }, + setupFiles: ["src/vitest.setup.ts"], + environment: "jsdom", + // an example of file based convention, + // you don't have to follow it + include: ["src/**/*.test.ts", "src/**/*.test.tsx"], + name: "unit", + }, }, - }, - setupFiles: ["src/vitest.setup.ts"], - include: ["src/**/*.test.ts", "src/**/*.test.tsx"], + { + plugins: [ + storybookTest({ + // The location of your Storybook config, main.js|ts + configDir: path.join(dirname, ".storybook"), + }), + ...vitePluginsConfig(configEnv).plugins!, + ], + test: { + name: "storybook", + browser: { + enabled: true, + // Make sure to install Playwright + provider: playwright(), + headless: true, + instances: [{ browser: "chromium" }], + }, + }, + }, + ], coverage: { reporter: ["html", "json"], include: ["src/**/*.{ts,tsx,js,jsx}"], From 2ac221661b1ca26d4f021b6d2e0081acf0bf60dd Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 17:43:34 +0200 Subject: [PATCH 013/100] review: Use Initializer instead of creating a component --- src/App.tsx | 2 -- src/LivekitLogLevelSync.tsx | 22 ---------------------- src/initializer.tsx | 17 +++++++++++++++++ src/main.tsx | 11 ----------- 4 files changed, 17 insertions(+), 35 deletions(-) delete mode 100644 src/LivekitLogLevelSync.tsx diff --git a/src/App.tsx b/src/App.tsx index e124b0bd..b87f587c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,7 +32,6 @@ import { type AppViewModel } from "./state/AppViewModel"; import { MediaDevicesContext } from "./MediaDevicesContext"; import { getUrlParams, HeaderStyle } from "./UrlParams"; import { AppBar } from "./AppBar"; -import { LivekitLogLevelSync } from "./LivekitLogLevelSync.tsx"; const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route); @@ -82,7 +81,6 @@ export const App: FC = ({ vm }) => { const content = loaded ? ( - } diff --git a/src/LivekitLogLevelSync.tsx b/src/LivekitLogLevelSync.tsx deleted file mode 100644 index 5d3916d2..00000000 --- a/src/LivekitLogLevelSync.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright 2026 Element Creations Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -// Syncs the livekit log level with the "Enable extended Livekit logs" developer setting. -import { type FC, useEffect } from "react"; -import { setLogLevel } from "livekit-client"; - -import { useSetting, enableExtendedLivekitLogs } from "./settings/settings.ts"; - -export const LivekitLogLevelSync: FC = () => { - const [extendedLivekitLogs] = useSetting(enableExtendedLivekitLogs); - - useEffect(() => { - setLogLevel(extendedLivekitLogs ? "trace" : "info"); - }, [extendedLivekitLogs]); - - return <>; -}; diff --git a/src/initializer.tsx b/src/initializer.tsx index 2bd6f577..7c6fc529 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -23,12 +23,17 @@ import { createRoutesFromChildren, matchRoutes, } from "react-router-dom"; +import { + setLogExtension as setLKLogExtension, + setLogLevel as setLKLogLevel, +} from "livekit-client"; import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; import { platform } from "./Platform"; import { isFailure } from "./utils/fetch"; import { initializeWidget } from "./widget"; +import { enableExtendedLivekitLogs } from "./settings/settings.ts"; // This generates a map of locale names to their URL (based on import.meta.url), which looks like this: // { @@ -189,6 +194,18 @@ export class Initializer { // Add the platform to the DOM, so CSS can query it document.body.setAttribute("data-platform", platform); + + // livekit logging configuration + setLKLogExtension((level, msg, context) => { + // we pass a synthetic logger name of "livekit" to the rageshake to make it easier to read + global.mx_rage_logger.log(level, "livekit", msg, context); + }); + + enableExtendedLivekitLogs.value$.subscribe((enabled) => { + setLKLogLevel(enabled ? "trace" : "info"); + }); + + window.setLKLogLevel = setLKLogLevel; } public static init(): Promise | null { diff --git a/src/main.tsx b/src/main.tsx index 6cbf75fa..8f64c680 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -15,10 +15,6 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import { logger } from "matrix-js-sdk/lib/logger"; -import { - setLogExtension as setLKLogExtension, - setLogLevel as setLKLogLevel, -} from "livekit-client"; import { App } from "./App"; import { init as initRageshake } from "./settings/rageshake"; @@ -26,16 +22,9 @@ import { Initializer } from "./initializer"; import { AppViewModel } from "./state/AppViewModel"; import { globalScope } from "./state/ObservableScope"; -window.setLKLogLevel = setLKLogLevel; - initRageshake().catch((e) => { logger.error("Failed to initialize rageshake", e); }); -setLKLogLevel("info"); -setLKLogExtension((level, msg, context) => { - // we pass a synthetic logger name of "livekit" to the rageshake to make it easier to read - global.mx_rage_logger.log(level, "livekit", msg, context); -}); logger.info(`Element Call ${import.meta.env.VITE_APP_VERSION || "dev"}`); From f1b61a9f1c13e48d48b7632b887348f1fa86960f Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 11 May 2026 18:07:40 +0200 Subject: [PATCH 014/100] update developer settings tab snapshot --- .../DeveloperSettingsTab.test.tsx.snap | 70 ++++++++++++++----- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap b/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap index af38685a..8d43b12b 100644 --- a/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap +++ b/src/settings/__snapshots__/DeveloperSettingsTab.test.tsx.snap @@ -186,6 +186,44 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = ` + +
+
+ + +
+
renders and matches snapshot 1`] = ` > @@ -203,9 +241,9 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = ` class="_controls_17lij_8" > renders and matches snapshot 1`] = ` Currently, no overwrite is set. Url from well-known or config is used. @@ -237,10 +275,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = ` class="_container_1ug7n_10" > renders and matches snapshot 1`] = ` > Compatible with old versions of EC that do not support multi SFU @@ -278,9 +316,9 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = ` class="_container_1ug7n_10" > renders and matches snapshot 1`] = ` > Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later) @@ -318,9 +356,9 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = ` class="_container_1ug7n_10" > renders and matches snapshot 1`] = ` > Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later From 53626785b35be902bc86eb3c3f93e3d1b3d7fc90 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 18:22:58 +0200 Subject: [PATCH 015/100] install playwright for unit tests --- .github/workflows/test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d8af6f92..3fca37a1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,6 +22,8 @@ jobs: - name: Install dependencies # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) run: "pnpm install --frozen-lockfile --ignore-pnpmfile" + - name: Install Playwright Browsers + run: pnpm exec playwright install - name: Vitest run: "pnpm run test:coverage" - name: Upload to codecov From bc2be4dfab8e017013a7b3b51d4ba0d26348d3c6 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 18:44:16 +0200 Subject: [PATCH 016/100] test in CI --- .github/workflows/test.yaml | 2 +- .storybook/vitest.setup.ts | 17 +++++++++++++++++ vitest.config.ts | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .storybook/vitest.setup.ts diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3fca37a1..b312d311 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,7 +23,7 @@ jobs: # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - name: Install Playwright Browsers - run: pnpm exec playwright install + run: pnpm exec playwright install --with-deps chromium - name: Vitest run: "pnpm run test:coverage" - name: Upload to codecov diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts new file mode 100644 index 00000000..de24c78c --- /dev/null +++ b/.storybook/vitest.setup.ts @@ -0,0 +1,17 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { beforeAll } from "vitest"; +import { setProjectAnnotations } from "@storybook/react-vite"; +import * as projectAnnotations from "./preview"; + +// Apply the right Storybook configuration when testing stories. +// See: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations +const annotations = setProjectAnnotations([projectAnnotations]); + +// Run Storybook's beforeAll hook (sets up decorators, parameters, etc.) +beforeAll(annotations.beforeAll); diff --git a/vitest.config.ts b/vitest.config.ts index 40fd4288..01ed73ad 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -39,6 +39,7 @@ export default defineConfig((configEnv) => ], test: { name: "storybook", + setupFiles: [".storybook/vitest.setup.ts"], browser: { enabled: true, // Make sure to install Playwright From 88f14ffcde62e154756d72d973ef521a26f1a304 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 18:56:52 +0200 Subject: [PATCH 017/100] try with playwirght container --- .github/workflows/test.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b312d311..2713a790 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -2,11 +2,15 @@ name: Test on: pull_request: {} push: - branches: [livekit, full-mesh] + branches: [livekit] jobs: vitest: name: Run unit tests runs-on: ubuntu-latest + container: + # Make sure to grab the latest version of the Playwright image + # https://playwright.dev/docs/docker#pull-the-image + image: mcr.microsoft.com/playwright:v1.59.1-noble steps: - name: Checkout code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -22,8 +26,8 @@ jobs: - name: Install dependencies # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps chromium + # - name: Install Playwright Browsers + # run: pnpm exec playwright install --with-deps chromium - name: Vitest run: "pnpm run test:coverage" - name: Upload to codecov From aa125d917d9340e60dae8632415acd09048f43da Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 19:01:47 +0200 Subject: [PATCH 018/100] no setup file for storybook --- vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index 01ed73ad..c800cf15 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -39,7 +39,7 @@ export default defineConfig((configEnv) => ], test: { name: "storybook", - setupFiles: [".storybook/vitest.setup.ts"], + // setupFiles: [".storybook/vitest.setup.ts"], browser: { enabled: true, // Make sure to install Playwright From 85762001a49d3ab1014e53081a65efca79e457ec Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 19:08:40 +0200 Subject: [PATCH 019/100] try file parallelism --- vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index c800cf15..20124ad8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,6 +12,7 @@ export default defineConfig((configEnv) => vitePluginsConfig(configEnv), defineConfig({ test: { + fileParallelism: false, projects: [ { extends: true, @@ -39,7 +40,6 @@ export default defineConfig((configEnv) => ], test: { name: "storybook", - // setupFiles: [".storybook/vitest.setup.ts"], browser: { enabled: true, // Make sure to install Playwright From 7ca890ea531138778062c46068b4c926d6112c46 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 19:08:45 +0200 Subject: [PATCH 020/100] Delete vitest.setup.ts --- .storybook/vitest.setup.ts | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .storybook/vitest.setup.ts diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts deleted file mode 100644 index de24c78c..00000000 --- a/.storybook/vitest.setup.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright 2026 Element Creations Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { beforeAll } from "vitest"; -import { setProjectAnnotations } from "@storybook/react-vite"; -import * as projectAnnotations from "./preview"; - -// Apply the right Storybook configuration when testing stories. -// See: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations -const annotations = setProjectAnnotations([projectAnnotations]); - -// Run Storybook's beforeAll hook (sets up decorators, parameters, etc.) -beforeAll(annotations.beforeAll); From f37128414a0a0e0bd2f3b7cb0fda8f5cb09eea18 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 19:16:07 +0200 Subject: [PATCH 021/100] try with firefox --- vitest.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index 20124ad8..9e8b05cf 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,6 +17,7 @@ export default defineConfig((configEnv) => { extends: true, test: { + name: "unit", css: { modules: { classNameStrategy: "non-scoped", @@ -27,7 +28,6 @@ export default defineConfig((configEnv) => // an example of file based convention, // you don't have to follow it include: ["src/**/*.test.ts", "src/**/*.test.tsx"], - name: "unit", }, }, { @@ -45,7 +45,7 @@ export default defineConfig((configEnv) => // Make sure to install Playwright provider: playwright(), headless: true, - instances: [{ browser: "chromium" }], + instances: [{ browser: "firefox" }], }, }, }, From 4f28bac749309aa08888c2cf47a51f34ffe2c06d Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 19:18:10 +0200 Subject: [PATCH 022/100] without playwright container --- .github/workflows/test.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2713a790..96483af2 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,10 +7,7 @@ jobs: vitest: name: Run unit tests runs-on: ubuntu-latest - container: - # Make sure to grab the latest version of the Playwright image - # https://playwright.dev/docs/docker#pull-the-image - image: mcr.microsoft.com/playwright:v1.59.1-noble + steps: - name: Checkout code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -26,8 +23,8 @@ jobs: - name: Install dependencies # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - # - name: Install Playwright Browsers - # run: pnpm exec playwright install --with-deps chromium + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps firefox - name: Vitest run: "pnpm run test:coverage" - name: Upload to codecov From 5bdbd80f371b5f8d77dd3233fe8c0d19eca432e2 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 19:29:07 +0200 Subject: [PATCH 023/100] try storybook only --- .github/workflows/test.yaml | 6 +++--- vitest.config.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 96483af2..bb8ddcb5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,10 +23,10 @@ jobs: - name: Install dependencies # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps firefox + - name: Install Playwright Browser + run: pnpm exec playwright install --with-deps chromium - name: Vitest - run: "pnpm run test:coverage" + run: "pnpm run test:storybook" - name: Upload to codecov uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 env: diff --git a/vitest.config.ts b/vitest.config.ts index 9e8b05cf..22b5e88a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -45,7 +45,7 @@ export default defineConfig((configEnv) => // Make sure to install Playwright provider: playwright(), headless: true, - instances: [{ browser: "firefox" }], + instances: [{ browser: "chromium" }], }, }, }, From ded6e4c1a06f6c082a5cdbb6803d560a860affe0 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 19:50:32 +0200 Subject: [PATCH 024/100] fix tests dep optimization issues --- .github/workflows/test.yaml | 2 +- .storybook/main.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bb8ddcb5..7bdeefc5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,7 +26,7 @@ jobs: - name: Install Playwright Browser run: pnpm exec playwright install --with-deps chromium - name: Vitest - run: "pnpm run test:storybook" + run: "pnpm run test:coverage" - name: Upload to codecov uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 env: diff --git a/.storybook/main.ts b/.storybook/main.ts index 9a3f0b53..e227ef76 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -11,5 +11,26 @@ const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], addons: ["@storybook/addon-docs", "@storybook/addon-vitest"], framework: "@storybook/react-vite", + // THIS IS IMPORTANT + // vitest runs without Vite's normal dependency optimization, so we need to manually include the polyfills for the stories to work. + // otherwise we will get: new dependencies optimized: ... + // and + // ``` + // [vitest] Vite unexpectedly reloaded a test. This may cause tests to fail, lead to flaky behaviour or duplicated test runs. + // For a stable experience, please add mentioned dependencies to your config's `optimizeDeps.include` field manually. + // ``` + // which breaks the storybook ci on the first and only run. + viteFinal(config) { + config.optimizeDeps = { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + "vite-plugin-node-polyfills/shims/buffer", + "vite-plugin-node-polyfills/shims/global", + "vite-plugin-node-polyfills/shims/process", + ], + }; + return config; + }, }; export default config; From 34486e64864e6a5ea01ac54fe1c894758e3cb71b Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 19:56:21 +0200 Subject: [PATCH 025/100] use container to skip playwright install --- .github/workflows/test.yaml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7bdeefc5..6bb0f309 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,7 +7,10 @@ jobs: vitest: name: Run unit tests runs-on: ubuntu-latest - + container: + # Make sure to grab the latest version of the Playwright image + # https://playwright.dev/docs/docker#pull-the-image + image: mcr.microsoft.com/playwright:v1.59.1-noble steps: - name: Checkout code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -23,8 +26,8 @@ jobs: - name: Install dependencies # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - - name: Install Playwright Browser - run: pnpm exec playwright install --with-deps chromium + # - name: Install Playwright Browser + # run: pnpm exec playwright install --with-deps chromium - name: Vitest run: "pnpm run test:coverage" - name: Upload to codecov @@ -38,6 +41,10 @@ jobs: name: Run end-to-end tests timeout-minutes: 60 runs-on: ubuntu-latest + container: + # Make sure to grab the latest version of the Playwright image + # https://playwright.dev/docs/docker#pull-the-image + image: mcr.microsoft.com/playwright:v1.59.1-noble steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: @@ -51,8 +58,8 @@ jobs: - name: Install dependencies # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) run: pnpm install --frozen-lockfile --ignore-pnpmfile - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps + # - name: Install Playwright Browsers + # run: pnpm exec playwright install --with-deps - name: Run backend components run: | docker compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml pull From d37c064df39f7780bf3b96bc75394287432ce07e Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 20:01:18 +0200 Subject: [PATCH 026/100] custom container does not work for playwright tests --- .github/workflows/test.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6bb0f309..2cec64f5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,8 +26,6 @@ jobs: - name: Install dependencies # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - # - name: Install Playwright Browser - # run: pnpm exec playwright install --with-deps chromium - name: Vitest run: "pnpm run test:coverage" - name: Upload to codecov @@ -41,10 +39,6 @@ jobs: name: Run end-to-end tests timeout-minutes: 60 runs-on: ubuntu-latest - container: - # Make sure to grab the latest version of the Playwright image - # https://playwright.dev/docs/docker#pull-the-image - image: mcr.microsoft.com/playwright:v1.59.1-noble steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: @@ -58,8 +52,8 @@ jobs: - name: Install dependencies # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) run: pnpm install --frozen-lockfile --ignore-pnpmfile - # - name: Install Playwright Browsers - # run: pnpm exec playwright install --with-deps + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps - name: Run backend components run: | docker compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml pull From 9f6ae4ba55d99a087ae3459ac97839a89709ddd8 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 17:27:38 +0200 Subject: [PATCH 027/100] small device selection refactor --- src/settings/DeviceSelection.tsx | 73 ++++++++++++++------------------ 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/src/settings/DeviceSelection.tsx b/src/settings/DeviceSelection.tsx index 197046c3..f189348b 100644 --- a/src/settings/DeviceSelection.tsx +++ b/src/settings/DeviceSelection.tsx @@ -5,14 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { - type ChangeEvent, - type FC, - type ReactElement, - type ReactNode, - useCallback, - useId, -} from "react"; +import { type ChangeEvent, type FC, useCallback, useId } from "react"; import { Heading, InlineField, @@ -20,8 +13,8 @@ import { RadioControl, Separator, } from "@vector-im/compound-web"; -import { Trans, useTranslation } from "react-i18next"; import { useObservableEagerState } from "observable-hooks"; +import { t } from "i18next"; import { type AudioOutputDeviceLabel, @@ -37,12 +30,39 @@ interface Props { numberedLabel: (number: number) => string; } +export function mediaDeviceLabelToString( + label: DeviceLabel | AudioOutputDeviceLabel, + numberedLabel: (number: number) => string, +): string { + let labelText = ""; + 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") + : t("settings.devices.default") + " (" + label.name + ")"; + break; + case "speaker": + labelText = t("settings.devices.loudspeaker"); + break; + case "earpiece": + labelText = t("settings.devices.handset"); + break; + } + return labelText; +} + export const DeviceSelection: FC = ({ device, title, numberedLabel, }) => { - const { t } = useTranslation(); const groupId = useId(); const available = useObservableEagerState(device.available$); const selectedId = useObservableEagerState(device.selected$)?.id; @@ -70,38 +90,7 @@ export const DeviceSelection: FC = ({
{[...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; - case "speaker": - labelText = t("settings.devices.loudspeaker"); - break; - case "earpiece": - labelText = t("settings.devices.handset"); - break; - } - + const labelText = mediaDeviceLabelToString(label, numberedLabel); return ( Date: Mon, 11 May 2026 17:29:41 +0200 Subject: [PATCH 028/100] Introduce ViewModel.ts --- src/state/ViewModel.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/state/ViewModel.ts diff --git a/src/state/ViewModel.ts b/src/state/ViewModel.ts new file mode 100644 index 00000000..12016cd4 --- /dev/null +++ b/src/state/ViewModel.ts @@ -0,0 +1,39 @@ +/* +Copyright 2026 Element Software Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { BehaviorSubject } from "rxjs"; + +import { useBehavior } from "../useBehavior"; +import { type Behavior } from "./Behavior"; + +export type ViewModel = { + [K in keyof Snapshot as `${string & K}$`]: Behavior; +}; + +export function useViewModel(vm: ViewModel): Snapshot { + const snapshot = {} as Snapshot; + for (const key in vm) { + const value$ = (vm as Record>)[key]; + const snapshotKey = key.slice(0, -1) as keyof Snapshot; + // we allow using hooks in a loop here because we know the shape of the vm is static and won't change between renders, so the order of hooks calls will always be the same. + // eslint-disable-next-line react-hooks/rules-of-hooks + snapshot[snapshotKey] = useBehavior(value$) as Snapshot[keyof Snapshot]; + } + return snapshot; +} + +export function createMockedViewModel( + snapshot: Snapshot, +): ViewModel { + const vm = {} as ViewModel; + for (const key in snapshot) { + (vm as Record>)[`${key}$`] = new BehaviorSubject( + snapshot[key], + ); + } + return vm; +} From 7615e146a53adf2191a8fad83c0bf2e4306b54a8 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 17:36:54 +0200 Subject: [PATCH 029/100] Use ViewModel approach for the CallFooterView + interaction tests (this is just imaginary. There is no view model yet.) --- locales/en/app.json | 1 - src/components/CallFooter.module.css | 4 - src/components/CallFooter.stories.tsx | 143 +++++++++++++++++++++----- src/components/CallFooter.tsx | 130 +++++++++++------------ 4 files changed, 185 insertions(+), 93 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index b51c6ed9..1d5eaa19 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -202,7 +202,6 @@ "camera_numbered": "Camera {{n}}", "change_device_button": "Change audio device", "default": "Default", - "default_named": "Default <2>({{name}})", "handset": "Handset", "loudspeaker": "Loudspeaker", "microphone": "Microphone", diff --git a/src/components/CallFooter.module.css b/src/components/CallFooter.module.css index adff99d5..228d7654 100644 --- a/src/components/CallFooter.module.css +++ b/src/components/CallFooter.module.css @@ -26,10 +26,6 @@ Please see LICENSE in the repository root for full details. ); } -.footer.hidden { - display: none; -} - .footer.overlay { /* Note that the footer is still position: sticky in this case so that certain tiles can move up out of the way of the footer when visible. */ diff --git a/src/components/CallFooter.stories.tsx b/src/components/CallFooter.stories.tsx index 7090a338..129c2b3a 100644 --- a/src/components/CallFooter.stories.tsx +++ b/src/components/CallFooter.stories.tsx @@ -5,18 +5,40 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { fn } from "storybook/test"; +import { expect, fn, userEvent, within } from "storybook/test"; import { BehaviorSubject } from "rxjs"; -import { type ReactNode } from "react"; +import { type JSX, type ReactNode } from "react"; import { Link } from "@vector-im/compound-web"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import { CallFooter, type FooterProps } from "./CallFooter"; +import { CallFooter, type FooterSnapshot } from "./CallFooter"; import inCallViewStyles from "../room/InCallView.module.css"; +import { createMockedViewModel } from "../state/ViewModel"; import { ReactionsSenderContext } from "../reactions/useReactionsSender"; import { type ReactionOption } from "../reactions"; +import { type GridMode } from "../state/CallViewModel/CallViewModel"; +// consts for tests +const reactionIdentifier = "@user:example.com:DEVICE"; +const reactionData = { + handsRaised$: new BehaviorSubject({}), + reactions$: new BehaviorSubject({}), +}; -function CallFooterWrapper(props: FooterProps): ReactNode { +/** + * A wrapper component that is used for: + * - exposing the snapshot via props so the storybook documents the snapshot properties (basically unpack them form the vm) + * - Add additional react context + * @param chilren used for the "Back to Recents" button in the lobby stories, but can be used for anything really + * @param vmSnapshot the Snapshot of the vm, the wrapper will create a mocked vm from it and pass it to the CallFooter. + * @returns + */ +function CallFooterStoryWrapper({ + children, + ...vmSnapshot +}: FooterSnapshot & { + children?: false | JSX.Element | JSX.Element[] | undefined; +}): ReactNode { + const vm = createMockedViewModel(vmSnapshot); return (
Promise.resolve(), }} > - +
); } const meta = { - component: CallFooterWrapper, -} satisfies Meta; + component: CallFooterStoryWrapper, +} satisfies Meta; export default meta; type Story = StoryObj; -const reactionIdentifier = "@user:example.com:DEVICE"; -const reactionData = { - handsRaised$: new BehaviorSubject({}), - reactions$: new BehaviorSubject({}), -}; - const fnArgType = { control: { type: "select" as const }, options: ["MockedCallback", "undefined"], mapping: { MockedCallback: fn(), undefined: undefined }, }; + export const Default: Story = { args: { - hideLogo: true, + showLogo: false, layoutMode: "grid", audioEnabled: true, videoEnabled: true, @@ -62,12 +79,16 @@ export const Default: Story = { toggleVideo: fn(), toggleScreenSharing: fn(), hangup: fn(), + buttonSize: "lg", }, parameters: { layout: "fullscreen", }, argTypes: { - layoutMode: { control: "radio", options: ["grid", "spotlight"] }, + layoutMode: { + control: "radio", + options: ["grid", "spotlight"] satisfies GridMode[], + }, audioOutputSwitcher: { control: "select", options: ["NoOutputCallback", "speaker", "earpiece"], @@ -110,7 +131,7 @@ export const WithLogo: Story = { ...Default, args: { ...Default.args, - hideLogo: false, + showLogo: true, }, }; @@ -121,6 +142,51 @@ export const AudioVideoEnabled: Story = { audioEnabled: true, videoEnabled: true, }, + play: async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + + const spotlightRadio = canvas.getByRole("radio", { name: "Spotlight" }); + await userEvent.click(spotlightRadio); + await expect(args.setLayoutMode).toHaveBeenCalledWith("spotlight"); + + const micButtonMute = canvas.getByRole("switch", { + name: "Mute microphone", + }); + await userEvent.click(micButtonMute); + await expect(args.toggleAudio).toHaveBeenCalled(); + + const videoMuteButton = canvas.getByRole("switch", { + name: "Stop video", + }); + await userEvent.click(videoMuteButton); + await expect(args.toggleVideo).toHaveBeenCalled(); + const screenShare = canvas.getByRole("switch", { + name: "Share screen", + }); + await userEvent.click(screenShare); + await expect(args.toggleScreenSharing).toHaveBeenCalled(); + const endCall = canvas.getByRole("button", { + name: "End call", + }); + await userEvent.click(endCall); + await expect(args.hangup).toHaveBeenCalled(); + }, +}; + +/** used to test switching to grid mode */ +export const SpotlightMode: Story = { + ...Default, + args: { + ...Default.args, + layoutMode: "spotlight", + }, + play: async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + + const spotlightRadio = canvas.getByRole("radio", { name: "Grid" }); + await userEvent.click(spotlightRadio); + await expect(args.setLayoutMode).toHaveBeenCalledWith("grid"); + }, }; export const WithAudioOutputSpeaker: Story = { @@ -150,7 +216,38 @@ export const Pip: Story = { ...Default, args: { ...Default.args, - asPip: true, + buttonSize: "md", + showSettingsButton: false, + layoutMode: undefined, + }, + play: async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + + await expect( + canvas.queryByRole("radio", { name: "Spotlight" }), + ).not.toBeInTheDocument(); + + const micButtonMute = canvas.getByRole("switch", { + name: "Mute microphone", + }); + await userEvent.click(micButtonMute); + await expect(args.toggleAudio).toHaveBeenCalled(); + + const videoMuteButton = canvas.getByRole("switch", { + name: "Stop video", + }); + await userEvent.click(videoMuteButton); + await expect(args.toggleVideo).toHaveBeenCalled(); + const screenShare = canvas.getByRole("switch", { + name: "Share screen", + }); + await userEvent.click(screenShare); + await expect(args.toggleScreenSharing).toHaveBeenCalled(); + const endCall = canvas.getByRole("button", { + name: "End call", + }); + await userEvent.click(endCall); + await expect(args.hangup).toHaveBeenCalled(); }, }; export const NoControlsWithLogo: Story = { @@ -158,7 +255,7 @@ export const NoControlsWithLogo: Story = { args: { ...Default.args, hideControls: true, - hideLogo: false, + showLogo: true, }, }; @@ -187,7 +284,7 @@ export const MobileLayout: Story = { ...Default, args: { ...Default.args, - hideLogo: true, + showLogo: false, audioOutputSwitcher: { targetOutput: "speaker", switch: fn() }, }, @@ -203,7 +300,7 @@ export const Lobby: Story = { ...Default, args: { ...Default.args, - hideLogo: true, + showLogo: false, openSettings: undefined, setLayoutMode: undefined, toggleScreenSharing: undefined, @@ -217,7 +314,7 @@ export const LobbyMobile: Story = { ...Default, args: { ...Default.args, - hideLogo: true, + showLogo: false, setLayoutMode: undefined, toggleScreenSharing: undefined, @@ -235,7 +332,7 @@ export const LobbyRecentButton: Story = { args: { ...Default.args, children: Back To Recents, - hideLogo: true, + showLogo: false, setLayoutMode: undefined, toggleScreenSharing: undefined, }, @@ -249,7 +346,7 @@ export const LobbyRecentButtonMobile: Story = { args: { ...Default.args, children: Back To Recents, - hideLogo: true, + showLogo: false, setLayoutMode: undefined, toggleScreenSharing: undefined, }, diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index afc5bdc9..bbb8f7e2 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -7,13 +7,12 @@ Please see LICENSE in the repository root for full details. import { type FC, type JSX, type Ref, useMemo } from "react"; import classNames from "classnames"; -import { BehaviorSubject } from "rxjs"; -import { Switch } from "@vector-im/compound-web"; -import { t } from "i18next"; import { SpotlightIcon, GridIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Switch } from "@vector-im/compound-web"; +import { t } from "i18next"; import LogoMark from "../icons/LogoMark.svg?react"; import LogoType from "../icons/LogoType.svg?react"; @@ -33,35 +32,36 @@ import { type GridMode } from "../state/CallViewModel/CallViewModel"; import { MediaMuteAndSwitchButton, type MenuOptions, + type ToggleOption, } from "./MediaMuteAndSwitchButton"; +import { type ViewModel, useViewModel } from "../state/ViewModel"; export interface AudioOutputSwitcher { targetOutput: string; switch: () => void; } -export interface FooterProps { - ref?: Ref; - /** Children will only be visible if the component is wider than 5*/ - children?: JSX.Element | JSX.Element[] | false; - +export interface FooterSnapshot { audioEnabled: boolean; /** Also controls if the audioMute button is disabled */ toggleAudio: (() => void) | undefined; + videoEnabled: boolean; /** Also controls if the videoMute button is disabled */ toggleVideo: (() => void) | undefined; /* This is needed for WindowMode = "flat" */ hideControls?: boolean; - /** hide the entire footer*/ - hidden?: boolean; - /** Pip controls buttonSize and hides: settings button, layout switcher and logo */ - asPip?: boolean; /** The footer should be used as an overlay. - * (Over the Call Grid) This saves spaces on small screens.*/ + * (Over the Call Grid) This saves spaces on small screens. */ asOverlay?: boolean; + buttonSize: "md" | "lg"; + showSettingsButton?: boolean; + showLayoutSwitcher?: boolean; + showLogoDebugContainer?: boolean; + showLogo?: boolean; + layoutMode?: GridMode; /** Also controls if the layout button is visible */ setLayoutMode?: (mode: GridMode) => void; @@ -69,7 +69,7 @@ export interface FooterProps { sharingScreen?: boolean; toggleScreenSharing?: () => void; - /** Also controls if the audio button is visible */ + /** Also controls if the audio output button is visible */ audioOutputSwitcher?: AudioOutputSwitcher; /** Also controls if the settings button is visible */ openSettings?: () => void; @@ -79,60 +79,64 @@ export interface FooterProps { reactionIdentifier?: string; reactionData?: ReactionData; - hideLogo?: boolean; // debug stuff debugTileLayout?: boolean; tileStoreGeneration?: number; + /** Providing no options `[]` or `undefined` will imply that we dont have a audio fast switcher */ audioOptions?: MenuOptions[]; + /** Providing no options `[]` or `undefined` will imply that we dont have a audio fast switcher */ videoOptions?: MenuOptions[]; selectedAudio?: string; selectedVideo?: string; - selectAudioDevice?: (deviceId: string) => void; - selectVideoDevice?: (deviceId: string) => void; + selectAudioButtonOption?: (deviceId: string) => void; + selectVideoButtonOption?: (option: string) => void; + videoToggles?: ToggleOption[]; } -export const CallFooter: FC = ({ - ref, - children, - asOverlay, - hidden, - hideControls, - hideLogo, - asPip, - layoutMode, - setLayoutMode, - openSettings, - audioEnabled, - videoEnabled, - toggleAudio, - toggleVideo, - sharingScreen, - toggleScreenSharing, - reactionIdentifier, - reactionData, - audioOutputSwitcher, - hangup, - debugTileLayout, - tileStoreGeneration, +export interface FooterProps { + ref?: Ref; + children?: JSX.Element | JSX.Element[] | false; + vm: ViewModel; +} +export const CallFooter: FC = ({ ref, children, vm }) => { + const { + asOverlay, + hideControls, + layoutMode, + setLayoutMode, + openSettings, + audioEnabled, + videoEnabled, + toggleAudio, + toggleVideo, + sharingScreen, + toggleScreenSharing, + reactionIdentifier, + reactionData, + audioOutputSwitcher, + hangup, + debugTileLayout, + tileStoreGeneration, + videoOptions, + selectedVideo, + audioOptions, + selectedAudio, + selectAudioButtonOption, + selectVideoButtonOption, + videoToggles, + buttonSize, + showSettingsButton, + showLogoDebugContainer, + showLogo, + } = useViewModel(vm); - audioOptions, - videoOptions, - selectedAudio, - selectedVideo, - selectAudioDevice, - selectVideoDevice, -}) => { const buttons: JSX.Element[] = []; - const buttonSize = asPip ? "md" : "lg"; - const showSettingsButton = - openSettings !== undefined && !asPip && !hideControls; - const showLayoutSwitcher = !asPip && !hideControls; - const showLogoDebugContainer = !asPip || (!hideLogo && !debugTileLayout); - const showLogo = !hideLogo && !asPip; + if (showSettingsButton) { - // add the settings button to the center group of buttons, so it will be visible on small screens. - // On larger screens, it will be hidden SettingsIconButton the one with `showForScreenWidth = "wide"` in the `settingsLogoContainer` will be visible. + // Add the settings button to the center group so it's visible on small + // screens. On larger screens the SettingsIconButton with + // showForScreenWidth="wide" in the settingsLogoContainer is used instead. buttons.push( = ({ data-testid="incall_mute" options={audioOptions} selectedOption={selectedAudio} - onSelect={selectAudioDevice} + onSelect={selectAudioButtonOption} />, ); } else { @@ -169,6 +173,7 @@ export const CallFooter: FC = ({ />, ); } + if ((videoOptions?.length ?? 0) > 0) { buttons.push( = ({ onMuteClick={toggleVideo} data-testid="incall_videomute" options={videoOptions} + toggles={videoToggles} selectedOption={selectedVideo} - onSelect={selectVideoDevice} + onSelect={selectVideoButtonOption} />, ); } else { @@ -213,12 +219,7 @@ export const CallFooter: FC = ({ buttons.push( = ({ ref={ref} className={classNames(styles.footer, { [styles.overlay]: asOverlay, - [styles.hidden]: hidden, })} >
@@ -288,7 +288,7 @@ export const CallFooter: FC = ({ {showLogoDebugContainer && logoDebugContainer}
{!hideControls &&
{buttons}
} - {setLayoutMode && layoutMode && showLayoutSwitcher && ( + {setLayoutMode && layoutMode && ( name="layoutMode" aria-label={t("layout_switch_label")} From 936dfb193f0988cfd4e2544a73b8383d625bd799 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 17:37:06 +0200 Subject: [PATCH 030/100] add the CallFooterViewModel --- src/components/CallFooterViewModel.test.ts | 143 ++++++++++ src/components/CallFooterViewModel.tsx | 302 +++++++++++++++++++++ 2 files changed, 445 insertions(+) create mode 100644 src/components/CallFooterViewModel.test.ts create mode 100644 src/components/CallFooterViewModel.tsx diff --git a/src/components/CallFooterViewModel.test.ts b/src/components/CallFooterViewModel.test.ts new file mode 100644 index 00000000..601a8393 --- /dev/null +++ b/src/components/CallFooterViewModel.test.ts @@ -0,0 +1,143 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { describe, expect, it, vi } from "vitest"; + +import { testScope, mockMuteStates, mockMediaDevices } from "../utils/test"; +import { constant } from "../state/Behavior"; +import type { CallViewModel } from "../state/CallViewModel/CallViewModel"; +import type { Layout } from "../state/layout-types"; +import type { SpotlightTileViewModel } from "../state/TileViewModel"; +import type { DeviceLabel } from "../state/MediaDevices"; +import { createCallFooterViewModel } from "./CallFooterViewModel"; + +const platformMock = vi.hoisted(() => vi.fn(() => "desktop")); +vi.mock("../Platform", () => ({ + get platform(): string { + return platformMock(); + }, +})); + +// Prevent supportsBackgroundProcessors from throwing in jsdom – it is not +// exercised by these tests (only used in `videoToggles`, not `videoOptions`). +vi.mock("@livekit/track-processors", () => ({ + supportsBackgroundProcessors: (): boolean => false, +})); + +/** + * Returns the minimum set of CallViewModel fields required by + * createCallFooterViewModel, with all other properties stubbed to + * simple constant values. + */ +function buildMinimalCallViewModel(layout: Layout): CallViewModel { + return { + layout$: constant(layout), + windowMode$: constant("normal"), + showHeader$: constant(false), + hangup: (): void => {}, + gridMode$: constant("grid"), + setGridMode: (): void => {}, + sharingScreen$: constant(false), + toggleScreenSharing: null, + audioOutputSwitcher$: constant(null), + handsRaised$: constant({}), + reactions$: constant({}), + tileStoreGeneration$: constant(0), + } as unknown as CallViewModel; +} + +/** A regular grid layout (not PiP). */ +const gridLayout: Layout = { + type: "grid", + grid: [], + setVisibleTiles: (): void => {}, +}; + +/** A PiP layout – only the `type` matters for the tests. */ +const pipLayout: Layout = { + type: "pip", + spotlight: {} as SpotlightTileViewModel, +}; + +const twoMicsAndOneCamMediaDevices = mockMediaDevices({ + audioInput: { + available$: constant( + new Map([ + ["mic1", { type: "name", name: "Microphone 1" }], + ["mic2", { type: "name", name: "Microphone 2" }], + ]), + ), + selected$: constant(undefined), + select: vi.fn(), + }, + videoInput: { + available$: constant( + new Map([ + ["cam1", { type: "name", name: "Camera 1" }], + ]), + ), + selected$: constant(undefined), + select: vi.fn(), + }, +}); + +describe("createCallFooterViewModel", () => { + describe("audioOptions and videoOptions", () => { + it("are empty when the layout is PiP on desktop", () => { + platformMock.mockReturnValue("desktop"); + + const vm = createCallFooterViewModel( + testScope(), + buildMinimalCallViewModel(pipLayout), + mockMuteStates(), + twoMicsAndOneCamMediaDevices, + /* openSettings */ undefined, + /* reactionIdentifier */ undefined, + ); + + expect(vm.audioOptions$?.value).toEqual([]); + expect(vm.videoOptions$?.value).toEqual([]); + }); + + it("are empty when the platform is iOS regardless of layout", () => { + platformMock.mockReturnValue("ios"); + + const vm = createCallFooterViewModel( + testScope(), + buildMinimalCallViewModel(gridLayout), + mockMuteStates(), + twoMicsAndOneCamMediaDevices, + /* openSettings */ undefined, + /* reactionIdentifier */ undefined, + ); + + expect(vm.audioOptions$?.value).toEqual([]); + expect(vm.videoOptions$?.value).toEqual([]); + }); + + it("are populated when the platform is desktop and the layout is not PiP", () => { + platformMock.mockReturnValue("desktop"); + + const vm = createCallFooterViewModel( + testScope(), + buildMinimalCallViewModel(gridLayout), + mockMuteStates(), + twoMicsAndOneCamMediaDevices, + /* openSettings */ undefined, + /* reactionIdentifier */ undefined, + ); + + expect(vm.audioOptions$?.value).toEqual([ + { id: "mic1", label: "Microphone 1" }, + { id: "mic2", label: "Microphone 2" }, + ]); + expect(vm.videoOptions$?.value).toEqual([ + { id: "cam1", label: "Camera 1" }, + ]); + }); + }); +}); diff --git a/src/components/CallFooterViewModel.tsx b/src/components/CallFooterViewModel.tsx new file mode 100644 index 00000000..c1ca11fa --- /dev/null +++ b/src/components/CallFooterViewModel.tsx @@ -0,0 +1,302 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { combineLatest, map, switchMap } from "rxjs"; +import { supportsBackgroundProcessors } from "@livekit/track-processors"; + +import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; +import { + type MenuOptions, + type ToggleOption, +} from "./MediaMuteAndSwitchButton"; +import { type MediaDevices } from "../state/MediaDevices"; +import { mediaDeviceLabelToString } from "../settings/DeviceSelection"; +import { + backgroundBlur as backgroundBlurSettings, + debugTileLayout as debugTileLayoutSetting, +} from "../settings/settings"; +import { type Behavior, constant } from "../state/Behavior"; +import type { ObservableScope } from "../state/ObservableScope"; +import { type MuteStates } from "../state/MuteStates"; +import { type ViewModel } from "../state/ViewModel"; +import { getUrlParams, HeaderStyle } from "../UrlParams"; +import { platform } from "../Platform"; +import { type FooterSnapshot } from "./CallFooter"; + +/** + * Shared helper: maps MuteStates into the audio/video enabled + toggle behaviors + * needed by FooterSnapshot. + */ +function buildMuteBehaviors( + scope: ObservableScope, + muteStates: MuteStates, +): Pick< + ViewModel, + "audioEnabled$" | "toggleAudio$" | "videoEnabled$" | "toggleVideo$" +> { + return { + audioEnabled$: muteStates.audio.enabled$, + toggleAudio$: scope.behavior( + muteStates.audio.toggle$.pipe(map((t) => t ?? undefined)), + ), + videoEnabled$: muteStates.video.enabled$, + toggleVideo$: scope.behavior( + muteStates.video.toggle$.pipe(map((t) => t ?? undefined)), + ), + }; +} + +/** + * Shared helper: maps MediaDevices into the audio/video device-list behaviors + * needed by FooterSnapshot (options, selection, callbacks, blur toggle). + */ +function buildDeviceBehaviors( + scope: ObservableScope, + mediaDevices: MediaDevices, + /** return empty arrays for audioOptions and videoOptions*/ + disableSwitcher$: Behavior, +): Pick< + ViewModel, + | "audioOptions$" + | "selectedAudio$" + | "selectAudioButtonOption$" + | "videoOptions$" + | "selectedVideo$" + | "selectVideoButtonOption$" + | "videoToggles$" +> { + return { + audioOptions$: scope.behavior( + disableSwitcher$.pipe( + switchMap((disable) => + disable + ? constant([] as MenuOptions[]) + : mediaDevices.audioInput.available$.pipe( + map((available) => + [...available.entries()].map(([id, label]) => ({ + id, + label: mediaDeviceLabelToString( + label, + (n) => "Audio Device " + n, + ), + })), + ), + ), + ), + ), + ), + selectedAudio$: scope.behavior( + mediaDevices.audioInput.selected$.pipe(map((s) => s?.id)), + ), + selectAudioButtonOption$: constant(mediaDevices.audioInput.select), + videoOptions$: scope.behavior( + disableSwitcher$.pipe( + switchMap((disable) => + disable + ? constant([] as MenuOptions[]) + : mediaDevices.videoInput.available$.pipe( + map((available) => + [...available.entries()].map(([id, label]) => ({ + id, + label: mediaDeviceLabelToString( + label, + (n) => "Camera " + n, + ), + })), + ), + ), + ), + ), + ), + selectedVideo$: scope.behavior( + mediaDevices.videoInput.selected$.pipe(map((s) => s?.id)), + ), + selectVideoButtonOption$: scope.behavior( + backgroundBlurSettings.value$.pipe( + map((current) => { + return (option: string) => { + if (option === "blur") { + backgroundBlurSettings.setValue(!current); + } else { + mediaDevices.videoInput.select(option); + } + }; + }), + ), + ), + videoToggles$: scope.behavior( + disableSwitcher$.pipe( + switchMap((disable) => + disable + ? constant([] as ToggleOption[]) + : backgroundBlurSettings.value$.pipe( + map((blurActive) => + supportsBackgroundProcessors() + ? [ + { + id: "blur", + enabled: blurActive, + label: "Blur Background", + }, + ] + : [], + ), + ), + ), + ), + ), + }; +} + +/** + * Creates the ViewModel for the CallFooter. + * + * @param scope - ObservableScope that bounds the lifetime of derived behaviors. + * @param vm - The root CallViewModel; provides layout, grid mode, reactions, etc. + * @param muteStates - Audio and video mute state + toggles. + * @param mediaDevices - Available and selected input devices. + * @param openSettings - Callback to open the settings modal, or undefined if the + * settings button should be hidden (e.g. when it is already shown in an app bar). + * @param reactionIdentifier - The local user's reaction identifier string, or + * undefined when reactions are not supported (hides the reaction button). + */ +export function createCallFooterViewModel( + scope: ObservableScope, + callModel: CallViewModel, + muteStates: MuteStates, + mediaDevices: MediaDevices, + openSettings: (() => void) | undefined, + reactionIdentifier: string | undefined, +): ViewModel { + const { showControls, header: headerStyle } = getUrlParams(); + + const hideLogo = headerStyle !== HeaderStyle.Standard; + const isPip$ = scope.behavior( + callModel.layout$.pipe(map((l) => l.type === "pip")), + ); + const disableDeviceSwitcher$ = scope.behavior( + isPip$.pipe(map((isPip) => isPip || platform !== "desktop")), + ); + return { + ...buildMuteBehaviors(scope, muteStates), + ...buildDeviceBehaviors(scope, mediaDevices, disableDeviceSwitcher$), + + hideControls$: constant(!showControls), + asOverlay$: scope.behavior( + callModel.windowMode$.pipe(map((mode) => mode === "flat")), + ), + buttonSize$: scope.behavior( + isPip$.pipe(map((pip) => (pip ? "md" : "lg") as "md" | "lg")), + ), + showSettingsButton$: scope.behavior( + combineLatest([isPip$, callModel.showHeader$]).pipe( + map( + ([isPip, showHeader]) => + openSettings !== undefined && + !isPip && + showControls && + !(headerStyle === HeaderStyle.AppBar && showHeader), + ), + ), + ), + showLayoutSwitcher$: scope.behavior( + isPip$.pipe(map((l) => !isPip$ && showControls)), + ), + showLogoDebugContainer$: scope.behavior( + combineLatest([isPip$, debugTileLayoutSetting.value$]).pipe( + map(([isPip, debugTile]) => !isPip || (!hideLogo && !debugTile)), + ), + ), + showLogo$: scope.behavior(isPip$.pipe(map((l) => !hideLogo && !isPip$))), + + layoutMode$: callModel.gridMode$, + setLayoutMode$: constant(callModel.setGridMode), + + sharingScreen$: callModel.sharingScreen$, + toggleScreenSharing$: constant(callModel.toggleScreenSharing ?? undefined), + + audioOutputSwitcher$: scope.behavior( + callModel.audioOutputSwitcher$.pipe( + map((switcher) => switcher ?? undefined), + ), + ), + + openSettings$: scope.behavior( + callModel.showHeader$.pipe( + map((showHeader) => + headerStyle === HeaderStyle.AppBar && showHeader + ? undefined + : openSettings, + ), + ), + ), + hangup$: constant(callModel.hangup), + + reactionIdentifier$: constant(reactionIdentifier), + reactionData$: constant( + reactionIdentifier !== undefined + ? { + handsRaised$: callModel.handsRaised$, + reactions$: callModel.reactions$, + } + : undefined, + ), + + debugTileLayout$: debugTileLayoutSetting.value$, + tileStoreGeneration$: callModel.tileStoreGeneration$, + }; +} + +/** + * Creates a simplified ViewModel for the CallFooter used in the lobby + * (pre-call) screen. Unlike createCallFooterViewModel, this does not require + * a CallViewModel — it only needs mute states, device lists, and callbacks. + * + * @param scope - ObservableScope that bounds the lifetime of derived behaviors. + * @param muteStates - Audio and video mute state + toggles. + * @param mediaDevices - Available and selected input devices. + * @param openSettings - Callback to open the settings modal, or undefined. + * @param hangup - Callback to leave/cancel, or undefined (hides the button). + * @param showLogo - Whether to show the Element Call logo. + */ +export function createLobbyFooterViewModel( + scope: ObservableScope, + muteStates: MuteStates, + mediaDevices: MediaDevices, + openSettings: (() => void) | undefined, + hangup: (() => void) | undefined, + showLogo: boolean, +): ViewModel { + return { + ...buildMuteBehaviors(scope, muteStates), + ...buildDeviceBehaviors(scope, mediaDevices, constant(false)), + hideControls$: constant(false), + asOverlay$: constant(false), + buttonSize$: constant("lg"), + showSettingsButton$: constant(openSettings !== undefined), + showLayoutSwitcher$: constant(false), + showLogoDebugContainer$: constant(showLogo), + showLogo$: constant(showLogo), + + layoutMode$: constant(undefined), + setLayoutMode$: constant(undefined), + + sharingScreen$: constant(undefined), + toggleScreenSharing$: constant(undefined), + + audioOutputSwitcher$: constant(undefined), + + openSettings$: constant(openSettings), + hangup$: constant(hangup), + + reactionIdentifier$: constant(undefined), + reactionData$: constant(undefined), + + debugTileLayout$: constant(false), + tileStoreGeneration$: constant(0), + }; +} From d29b92058109af55a7ab5f94b52163eea1fb8d5c Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 11 May 2026 17:53:09 +0200 Subject: [PATCH 031/100] use footer view model in lobby --- src/room/LobbyView.tsx | 44 +++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 277ed61c..cec9f6ac 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -38,6 +38,7 @@ import { useMediaQuery } from "../useMediaQuery"; import { E2eeType } from "../e2ee/e2eeType"; import { Link } from "../button/Link"; import { useMediaDevices } from "../MediaDevicesContext"; +import { ObservableScope } from "../state/ObservableScope"; import { useInitial } from "../useInitial"; import { useTrackProcessor, @@ -46,8 +47,10 @@ import { import { usePageTitle } from "../usePageTitle"; import { getValue } from "../utils/observable"; import { useBehavior } from "../useBehavior"; -import { CallFooter } from "../components/CallFooter"; +import { CallFooter, type FooterSnapshot } from "../components/CallFooter"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; +import { createLobbyFooterViewModel } from "../components/CallFooterViewModel"; +import { type ViewModel } from "../state/ViewModel"; interface Props { client: MatrixClient; @@ -112,6 +115,7 @@ export const LobbyView: FC = ({ logger.error("Failed to navigate to /", error); }); }, [navigate]); + const hangup = confineToRoom ? undefined : onLeaveClick; const recentsButtonInFooter = useMediaQuery("(max-height: 500px)"); const recentsButton = !confineToRoom && ( @@ -184,6 +188,27 @@ export const LobbyView: FC = ({ useTrackProcessorSync(videoTrack); + const [footerVm, setFooterVm] = useState | null>( + null, + ); + useEffect(() => { + const footerScope = new ObservableScope(); + setFooterVm( + createLobbyFooterViewModel( + footerScope, + muteStates, + devices, + openSettings, + hangup, + // Logo and header are connected: only show the logo in SPA with header. + !hideHeader, + ), + ); + return (): void => { + footerScope.end(); + }; + }, [devices, hangup, hideHeader, muteStates, onLeaveClick, openSettings]); + // TODO: Unify this component with InCallView, so we can get slick joining // animations and don't have to feel bad about reusing its CSS return ( @@ -227,18 +252,11 @@ export const LobbyView: FC = ({ {!recentsButtonInFooter && recentsButton}
- - {recentsButtonInFooter && recentsButton} - + {footerVm !== null && ( + + {recentsButtonInFooter && recentsButton} + + )} {client && ( Date: Mon, 11 May 2026 17:53:24 +0200 Subject: [PATCH 032/100] use footer view model in InCallview --- src/room/InCallView.tsx | 77 +++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 7fc97e27..932bb40d 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -43,7 +43,6 @@ import { InviteButton } from "../button/InviteButton"; import { type CallViewModel, createCallViewModel$, - type GridMode, } from "../state/CallViewModel/CallViewModel.ts"; import { Grid, type TileProps } from "../grid/Grid"; import { useInitial } from "../useInitial"; @@ -68,11 +67,7 @@ import { import { ReactionsAudioRenderer } from "./ReactionAudioRenderer"; import { ReactionsOverlay } from "./ReactionsOverlay"; import { CallEventAudioRenderer } from "./CallEventAudioRenderer"; -import { - debugTileLayout as debugTileLayoutSetting, - matrixRTCMode as matrixRTCModeSetting, - useSetting, -} from "../settings/settings"; +import { matrixRTCMode as matrixRTCModeSetting } from "../settings/settings"; import { ReactionsReader } from "../reactions/ReactionsReader"; import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx"; import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts"; @@ -90,8 +85,10 @@ import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.t import { type Layout } from "../state/layout-types.ts"; import { ObservableScope } from "../state/ObservableScope.ts"; import { useLatest } from "../useLatest.ts"; -import { CallFooter } from "../components/CallFooter.tsx"; +import { CallFooter, type FooterSnapshot } from "../components/CallFooter.tsx"; import { SettingsIconButton } from "../button/Button.tsx"; +import { createCallFooterViewModel } from "../components/CallFooterViewModel.tsx"; +import { type ViewModel } from "../state/ViewModel.ts"; const logger = rootLogger.getChild("[InCallView]"); @@ -220,9 +217,7 @@ export const InCallView: FC = ({ muted: muteAllAudio, }); const latestPickupPhaseAudio = useLatest(pickupPhaseAudio); - - const audioEnabled = useBehavior(muteStates.audio.enabled$); - const videoEnabled = useBehavior(muteStates.video.enabled$); + const mediaDevices = useMediaDevices(); const toggleAudio = useBehavior(muteStates.audio.toggle$); const toggleVideo = useBehavior(muteStates.video.toggle$); const setAudioEnabled = useBehavior(muteStates.audio.setEnabled$); @@ -241,14 +236,10 @@ export const InCallView: FC = ({ const reconnecting = useBehavior(vm.reconnecting$); const windowMode = useBehavior(vm.windowMode$); const layout = useBehavior(vm.layout$); - const tileStoreGeneration = useBehavior(vm.tileStoreGeneration$); - const [debugTileLayout] = useSetting(debugTileLayoutSetting); - const gridMode = useBehavior(vm.gridMode$); const showHeader = useBehavior(vm.showHeader$); const showFooter = useBehavior(vm.showFooter$); const earpieceMode = useBehavior(vm.earpieceMode$); const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$); - const sharingScreen = useBehavior(vm.sharingScreen$); const fatalCallError = useBehavior(vm.fatalError$); // Stop the rendering and throw for the error boundary @@ -348,11 +339,6 @@ export const InCallView: FC = ({ () => new BehaviorSubject(defaultPipAlignment), ); - const setGridMode = useCallback( - (mode: GridMode) => vm.setGridMode(mode), - [vm], - ); - useAppBarHidden(!showHeader); let header: ReactNode = null; @@ -558,9 +544,28 @@ export const InCallView: FC = ({ const rageshakeRequestModalProps = useRageshakeRequestModal( matrixRoom.roomId, ); + const [footerVm, setFooterVm] = useState | null>( + null, + ); + useEffect(() => { + const footerScope = new ObservableScope(); + setFooterVm( + createCallFooterViewModel( + footerScope, + vm, + muteStates, + mediaDevices, + openSettings, + supportsReactions + ? `${client.getUserId()}:${client.getDeviceId()}` + : undefined, + ), + ); + return (): void => { + footerScope.end(); + }; + }, [client, mediaDevices, muteStates, openSettings, supportsReactions, vm]); - const settingsButtonInAppBar = - headerStyle === HeaderStyle.AppBar && showHeader; useAppBarSecondaryButton( = ({ ); // Only hide the settings button if we have an AppBar header and we are showing the header - const footer = ( -