Merge branch 'livekit' into robin/berry

This commit is contained in:
Robin
2025-03-27 14:07:24 -04:00
22 changed files with 409 additions and 306 deletions

View File

@@ -165,6 +165,10 @@ jobs:
- name: Copy files - name: Copy files
run: rsync -a --delete --exclude .git element-call/embedded/ios/ element-call-swift run: rsync -a --delete --exclude .git element-call/embedded/ios/ element-call-swift
- name: Test build
working-directory: element-call-swift
run: swift build
- name: Get artifact version - name: Get artifact version
run: echo "ARTIFACT_VERSION=${VERSION:1}" >> "$GITHUB_ENV" run: echo "ARTIFACT_VERSION=${VERSION:1}" >> "$GITHUB_ENV"

View File

@@ -97,14 +97,14 @@ deployment for three different sites A, B and C is depicted below.
MatrixRTC backend (according to MatrixRTC backend (according to
[MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)) [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143))
is announced by the homeserver's `.well-known/matrix/client` file and discovered is announced by the Matrix site's `.well-known/matrix/client` file and discovered
via the `org.matrix.msc4143.rtc_foci` key, e.g.: via the `org.matrix.msc4143.rtc_foci` key, e.g.:
```json ```json
"org.matrix.msc4143.rtc_foci": [ "org.matrix.msc4143.rtc_foci": [
{ {
"type": "livekit", "type": "livekit",
"livekit_service_url": "https://someurl.com" "livekit_service_url": "https://matrix-rtc.example.com/livekit/jwt"
}, },
] ]
``` ```
@@ -232,8 +232,8 @@ The easiest way to develop new test is to use the codegen feature of Playwright:
npx playwright codegen npx playwright codegen
``` ```
This will record your action and write the test code for you. Use the tool bar to test visibility, text content, This will record your action and write the test code for you. Use the tool bar
clicking. to test visibility, text content and clicking.
##### Investigate a failed test from the CI ##### Investigate a failed test from the CI
@@ -251,8 +251,8 @@ Unzip the report then use this command to open the report in your browser:
npx playwright show-report ~/Downloads/playwright-report/ npx playwright show-report ~/Downloads/playwright-report/
``` ```
Under the failed test there is a small icon looking like "3 columns" (next to test name file name), Under the failed test there is a small icon looking like "3 columns" (next to
click on it to see the live screenshots/console output. the test name file name), click on it to see the live screenshots/console output.
### Test Coverage ### Test Coverage

View File

@@ -5,12 +5,6 @@
"server_name": "call-unstable.ems.host" "server_name": "call-unstable.ems.host"
} }
}, },
"livekit": {
"livekit_service_url": "https://livekit-jwt.call.element.dev"
},
"features": {
"feature_use_device_session_member_events": true
},
"posthog": { "posthog": {
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU", "api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",
"api_host": "https://posthog-element-call.element.io" "api_host": "https://posthog-element-call.element.io"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 909 KiB

After

Width:  |  Height:  |  Size: 929 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 KiB

After

Width:  |  Height:  |  Size: 886 KiB

View File

@@ -30,8 +30,9 @@ required for Element Call to work properly:
sync v2 API that allows them to correctly track the state of the room. This is sync v2 API that allows them to correctly track the state of the room. This is
required by Element Call to track room state reliably. required by Element Call to track room state reliably.
If you're using [Synapse](https://github.com/element-hq/synapse/) as your homeserver, you'll need If you're using [Synapse](https://github.com/element-hq/synapse/) as your
to additionally add the following config items to `homeserver.yaml` to comply with Element Call: homeserver, you'll need to additionally add the following config items to
`homeserver.yaml` to comply with Element Call:
```yaml ```yaml
experimental_features: experimental_features:
@@ -64,35 +65,88 @@ required for each site deployment.
![MSC4195 compatible setup](MSC4195_setup.drawio.png) ![MSC4195 compatible setup](MSC4195_setup.drawio.png)
As depicted above, Element Call requires a As depicted above in the `example.com` site deployment, Element Call requires a
[Livekit SFU](https://github.com/livekit/livekit) alongside a [Livekit SFU](https://github.com/livekit/livekit) alongside a
[Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service) [Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service)
to implement to implement
[MSC4195: MatrixRTC using LiveKit backend](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md). [MSC4195: MatrixRTC using LiveKit backend](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md).
#### Matrix site endpoint routing
In the context of MatrixRTC, we suggest using a single hostname for backend
communication by implementing endpoint routing within a reverse proxy setup. For
the example above, this results in:
| Service | Endpoint | Example |
| -------- | ------- | ------- |
| [Livekit SFU](https://github.com/livekit/livekit) WebSocket signalling connection | `/livekit/sfu` | `matrix-rtc.example.com/livekit/sfu` |
| [Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service) | `/livekit/jwt` | `matrix-rtc.example.com/livekit/jwt` |
Using Nginx, you can achieve this by:
```jsonc
server {
...
location ^~ /livekit/jwt/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# JWT Service running at port 8080
proxy_pass http://localhost:8080/;
}
location ^~ /livekit/sfu/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
proxy_set_header Accept-Encoding gzip;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# LiveKit SFU websocket connection running at port 7880
proxy_pass http://localhost:7880/;
}
}
```
#### MatrixRTC backend announcement
> [!IMPORTANT] > [!IMPORTANT]
> As defined in > As defined in
> [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143) > [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
> MatrixRTC backend must be announced to the client via your **homeserver's > MatrixRTC backend must be announced to the client via your **Matrix site's
> `.well-known/matrix/client`**. The configuration is a list of Foci configs: > `.well-known/matrix/client`** file (e.g.
> `example.com/.well-known/matrix/client` matching the site deployment example
> from above). The configuration is a list of Foci configs:
```json ```json
"org.matrix.msc4143.rtc_foci": [ "org.matrix.msc4143.rtc_foci": [
{ {
"type": "livekit", "type": "livekit",
"livekit_service_url": "https://someurl.com" "livekit_service_url": "https://matrix-rtc.example.com"
},
{
"type": "livekit",
"livekit_service_url": "https://livekit2.com"
}, },
{ {
"type": "another_foci", "type": "livekit",
"props_for_another_foci": "val" "livekit_service_url": "https://matrix-rtc-2.example.com"
},
{
"type": "nextgen_new_foci_type",
"props_for_nextgen_foci": "val"
} }
] ]
``` ```
> [!NOTE]
> Most `org.matrix.msc4143.rtc_foci` configurations will only have one entry in
> the array
## Building Element Call ## Building Element Call
> [!NOTE] > [!NOTE]

View File

@@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "EmbeddedElementCall", name: "EmbeddedElementCall",
platforms: [.iOS(.v17_6)], platforms: [.iOS(.v17)],
products: [ products: [
.library( .library(
name: "EmbeddedElementCall", name: "EmbeddedElementCall",

View File

@@ -50,7 +50,7 @@ export type ValidClientState = {
reactions: boolean; reactions: boolean;
thumbnails: boolean; thumbnails: boolean;
}; };
setClient: (params?: SetClientParams) => void; setClient: (client: MatrixClient, session: Session) => void;
}; };
export type AuthenticatedClient = { export type AuthenticatedClient = {
@@ -65,11 +65,6 @@ export type ErrorState = {
error: Error; error: Error;
}; };
export type SetClientParams = {
client: MatrixClient;
session: Session;
};
const ClientContext = createContext<ClientState | undefined>(undefined); const ClientContext = createContext<ClientState | undefined>(undefined);
export const ClientContextProvider = ClientContext.Provider; export const ClientContextProvider = ClientContext.Provider;
@@ -79,7 +74,7 @@ export const useClientState = (): ClientState | undefined =>
export function useClient(): { export function useClient(): {
client?: MatrixClient; client?: MatrixClient;
setClient?: (params?: SetClientParams) => void; setClient?: (client: MatrixClient, session: Session) => void;
} { } {
let client; let client;
let setClient; let setClient;
@@ -96,7 +91,7 @@ export function useClient(): {
// Plain representation of the `ClientContext` as a helper for old components that expected an object with multiple fields. // Plain representation of the `ClientContext` as a helper for old components that expected an object with multiple fields.
export function useClientLegacy(): { export function useClientLegacy(): {
client?: MatrixClient; client?: MatrixClient;
setClient?: (params?: SetClientParams) => void; setClient?: (client: MatrixClient, session: Session) => void;
passwordlessUser: boolean; passwordlessUser: boolean;
loading: boolean; loading: boolean;
authenticated: boolean; authenticated: boolean;
@@ -160,7 +155,11 @@ export const ClientProvider: FC<Props> = ({ children }) => {
initializing.current = true; initializing.current = true;
loadClient() loadClient()
.then(setInitClientState) .then((initResult) => {
setInitClientState(initResult);
if (PosthogAnalytics.instance.isEnabled())
PosthogAnalytics.instance.startListeningToSettingsChanges();
})
.catch((err) => logger.error(err)) .catch((err) => logger.error(err))
.finally(() => (initializing.current = false)); .finally(() => (initializing.current = false));
}, []); }, []);
@@ -196,24 +195,20 @@ export const ClientProvider: FC<Props> = ({ children }) => {
); );
const setClient = useCallback( const setClient = useCallback(
(clientParams?: SetClientParams) => { (client: MatrixClient, session: Session) => {
const oldClient = initClientState?.client; const oldClient = initClientState?.client;
const newClient = clientParams?.client; if (oldClient && oldClient !== client) {
if (oldClient && oldClient !== newClient) {
oldClient.stopClient(); oldClient.stopClient();
} }
if (clientParams) { saveSession(session);
saveSession(clientParams.session); setInitClientState({
setInitClientState({ widgetApi: null,
widgetApi: null, client,
client: clientParams.client, passwordlessUser: session.passwordlessUser,
passwordlessUser: clientParams.session.passwordlessUser, });
}); if (PosthogAnalytics.instance.isEnabled())
} else { PosthogAnalytics.instance.startListeningToSettingsChanges();
clearSession();
setInitClientState(null);
}
}, },
[initClientState?.client], [initClientState?.client],
); );
@@ -229,6 +224,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
clearSession(); clearSession();
setInitClientState(null); setInitClientState(null);
await navigate("/"); await navigate("/");
PosthogAnalytics.instance.logout();
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest); PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
}, [navigate, initClientState?.client]); }, [navigate, initClientState?.client]);

View File

@@ -12,6 +12,7 @@ import posthog, {
} from "posthog-js"; } from "posthog-js";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { type Subscription } from "rxjs";
import { widget } from "../widget"; import { widget } from "../widget";
import { import {
@@ -95,6 +96,7 @@ export class PosthogAnalytics {
private anonymity = Anonymity.Disabled; private anonymity = Anonymity.Disabled;
private platformSuperProperties = {}; private platformSuperProperties = {};
private registrationType: RegistrationType = RegistrationType.Guest; private registrationType: RegistrationType = RegistrationType.Guest;
private optInListener: Subscription | null = null;
public static hasInstance(): boolean { public static hasInstance(): boolean {
return Boolean(this.internalInstance); return Boolean(this.internalInstance);
@@ -143,7 +145,6 @@ export class PosthogAnalytics {
); );
this.enabled = false; this.enabled = false;
} }
this.startListeningToSettingsChanges(); // Triggers maybeIdentifyUser
} }
private sanitizeProperties = ( private sanitizeProperties = (
@@ -325,6 +326,8 @@ export class PosthogAnalytics {
if (this.enabled) { if (this.enabled) {
this.posthog.reset(); this.posthog.reset();
} }
this.optInListener?.unsubscribe();
this.optInListener = null;
this.setAnonymity(Anonymity.Disabled); this.setAnonymity(Anonymity.Disabled);
} }
@@ -403,7 +406,7 @@ export class PosthogAnalytics {
} }
} }
private startListeningToSettingsChanges(): void { public startListeningToSettingsChanges(): void {
// Listen to account data changes from sync so we can observe changes to relevant flags and update. // Listen to account data changes from sync so we can observe changes to relevant flags and update.
// This is called - // This is called -
// * On page load, when the account data is first received by sync // * On page load, when the account data is first received by sync
@@ -412,7 +415,7 @@ export class PosthogAnalytics {
// * When the user changes their preferences on this device // * When the user changes their preferences on this device
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings // Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes) // won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
optInAnalytics.value$.subscribe((optIn) => { this.optInListener ??= optInAnalytics.value$.subscribe((optIn) => {
this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled); this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled);
this.maybeIdentifyUser().catch(() => this.maybeIdentifyUser().catch(() =>
logger.log("Could not identify user"), logger.log("Could not identify user"),

View File

@@ -53,7 +53,7 @@ export const LoginPage: FC = () => {
return; return;
} }
setClient({ client, session }); setClient(client, session);
const locationState = location.state; const locationState = location.state;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@@ -95,7 +95,7 @@ export const RegisterPage: FC = () => {
} }
} }
setClient?.({ client: newClient, session }); setClient?.(newClient, session);
PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date()); PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date());
}; };

View File

@@ -47,7 +47,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
recaptchaResponse, recaptchaResponse,
true, true,
); );
setClient({ client, session }); setClient(client, session);
} catch (e) { } catch (e) {
reset(); reset();
throw e; throw e;

View File

@@ -89,7 +89,7 @@ export const UnauthenticatedView: FC = () => {
// @ts-ignore // @ts-ignore
if (error.errcode === "M_ROOM_IN_USE") { if (error.errcode === "M_ROOM_IN_USE") {
setOnFinished(() => { setOnFinished(() => {
setClient({ client, session }); setClient(client, session);
const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName); const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName);
navigate(`/${aliasLocalpart}`)?.catch((error) => { navigate(`/${aliasLocalpart}`)?.catch((error) => {
logger.error("Failed to navigate to alias localpart", error); logger.error("Failed to navigate to alias localpart", error);
@@ -111,7 +111,7 @@ export const UnauthenticatedView: FC = () => {
if (!createRoomResult.password) if (!createRoomResult.password)
throw new Error("Failed to create room with shared secret"); throw new Error("Failed to create room with shared secret");
setClient({ client, session }); setClient(client, session);
await navigate( await navigate(
getRelativeRoomUrl( getRelativeRoomUrl(
createRoomResult.roomId, createRoomResult.roomId,

View File

@@ -12,9 +12,9 @@ import { useEffect, useState } from "react";
import { type LivekitFocus } from "matrix-js-sdk/src/matrixrtc/LivekitFocus"; import { type LivekitFocus } from "matrix-js-sdk/src/matrixrtc/LivekitFocus";
import { useActiveLivekitFocus } from "../room/useActiveFocus"; import { useActiveLivekitFocus } from "../room/useActiveFocus";
import { useGroupCallErrorBoundary } from "../room/useCallErrorBoundary.ts"; import { useErrorBoundary } from "../useErrorBoundary";
import { FailToGetOpenIdToken } from "../utils/errors.ts"; import { FailToGetOpenIdToken } from "../utils/errors";
import { doNetworkOperationWithRetry } from "../utils/matrix.ts"; import { doNetworkOperationWithRetry } from "../utils/matrix";
export interface SFUConfig { export interface SFUConfig {
url: string; url: string;
@@ -41,7 +41,7 @@ export function useOpenIDSFU(
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined); const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
const activeFocus = useActiveLivekitFocus(rtcSession); const activeFocus = useActiveLivekitFocus(rtcSession);
const { showGroupCallErrorBoundary } = useGroupCallErrorBoundary(); const { showErrorBoundary } = useErrorBoundary();
useEffect(() => { useEffect(() => {
if (activeFocus) { if (activeFocus) {
@@ -50,14 +50,14 @@ export function useOpenIDSFU(
setSFUConfig(sfuConfig); setSFUConfig(sfuConfig);
}, },
(e) => { (e) => {
showGroupCallErrorBoundary(new FailToGetOpenIdToken(e)); showErrorBoundary(new FailToGetOpenIdToken(e));
logger.error("Failed to get SFU config", e); logger.error("Failed to get SFU config", e);
}, },
); );
} else { } else {
setSFUConfig(undefined); setSFUConfig(undefined);
} }
}, [client, activeFocus, showGroupCallErrorBoundary]); }, [client, activeFocus, showErrorBoundary]);
return sfuConfig; return sfuConfig;
} }

View File

@@ -5,7 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { beforeEach, expect, type MockedFunction, test, vitest } from "vitest"; import {
beforeEach,
expect,
type MockedFunction,
onTestFinished,
test,
vi,
} from "vitest";
import { render, waitFor, screen } from "@testing-library/react"; import { render, waitFor, screen } from "@testing-library/react";
import { type MatrixClient } from "matrix-js-sdk/src/client"; import { type MatrixClient } from "matrix-js-sdk/src/client";
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
@@ -15,6 +22,7 @@ import { BrowserRouter } from "react-router-dom";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container"; import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
import { useState } from "react"; import { useState } from "react";
import { TooltipProvider } from "@vector-im/compound-web";
import { type MuteStates } from "./MuteStates"; import { type MuteStates } from "./MuteStates";
import { prefetchSounds } from "../soundUtils"; import { prefetchSounds } from "../soundUtils";
@@ -28,20 +36,33 @@ import {
MockRTCSession, MockRTCSession,
} from "../utils/test"; } from "../utils/test";
import { GroupCallView } from "./GroupCallView"; import { GroupCallView } from "./GroupCallView";
import { leaveRTCSession } from "../rtcSessionHelpers";
import { type WidgetHelpers } from "../widget"; import { type WidgetHelpers } from "../widget";
import { LazyEventEmitter } from "../LazyEventEmitter"; import { LazyEventEmitter } from "../LazyEventEmitter";
import { MatrixRTCFocusMissingError } from "../utils/errors";
vitest.mock("../soundUtils"); vi.mock("../soundUtils");
vitest.mock("../useAudioContext"); vi.mock("../useAudioContext");
vitest.mock("./InCallView"); vi.mock("./InCallView");
vi.mock("react-use-measure", () => ({
default: (): [() => void, object] => [(): void => {}, {}],
}));
vitest.mock("../rtcSessionHelpers", async (importOriginal) => { const enterRTCSession = vi.hoisted(() => vi.fn(async () => Promise.resolve()));
const leaveRTCSession = vi.hoisted(() =>
vi.fn(
async (
rtcSession: unknown,
cause: unknown,
promiseBeforeHangup = Promise.resolve(),
) => await promiseBeforeHangup,
),
);
vi.mock("../rtcSessionHelpers", async (importOriginal) => {
// TODO: perhaps there is a more elegant way to manage the type import here? // TODO: perhaps there is a more elegant way to manage the type import here?
// eslint-disable-next-line @typescript-eslint/consistent-type-imports // eslint-disable-next-line @typescript-eslint/consistent-type-imports
const orig = await importOriginal<typeof import("../rtcSessionHelpers")>(); const orig = await importOriginal<typeof import("../rtcSessionHelpers")>();
vitest.spyOn(orig, "leaveRTCSession"); return { ...orig, enterRTCSession, leaveRTCSession };
return orig;
}); });
let playSound: MockedFunction< let playSound: MockedFunction<
@@ -55,11 +76,11 @@ const roomMembers = new Map([carol].map((p) => [p.userId, p]));
const roomId = "!foo:bar"; const roomId = "!foo:bar";
beforeEach(() => { beforeEach(() => {
vitest.clearAllMocks(); vi.clearAllMocks();
(prefetchSounds as MockedFunction<typeof prefetchSounds>).mockResolvedValue({ (prefetchSounds as MockedFunction<typeof prefetchSounds>).mockResolvedValue({
sound: new ArrayBuffer(0), sound: new ArrayBuffer(0),
}); });
playSound = vitest.fn(); playSound = vi.fn();
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({ (useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
playSound, playSound,
}); });
@@ -75,7 +96,10 @@ beforeEach(() => {
); );
}); });
function createGroupCallView(widget: WidgetHelpers | null): { function createGroupCallView(
widget: WidgetHelpers | null,
joined = true,
): {
rtcSession: MockRTCSession; rtcSession: MockRTCSession;
getByText: ReturnType<typeof render>["getByText"]; getByText: ReturnType<typeof render>["getByText"];
} { } {
@@ -88,7 +112,7 @@ function createGroupCallView(widget: WidgetHelpers | null): {
const room = mockMatrixRoom({ const room = mockMatrixRoom({
relations: { relations: {
getChildEventsForEvent: () => getChildEventsForEvent: () =>
vitest.mocked({ vi.mocked({
getRelations: () => [], getRelations: () => [],
}), }),
} as unknown as RelationsContainer, } as unknown as RelationsContainer,
@@ -106,24 +130,27 @@ function createGroupCallView(widget: WidgetHelpers | null): {
localRtcMember, localRtcMember,
[], [],
).withMemberships(of([])); ).withMemberships(of([]));
rtcSession.joined = joined;
const muteState = { const muteState = {
audio: { enabled: false }, audio: { enabled: false },
video: { enabled: false }, video: { enabled: false },
} as MuteStates; } as MuteStates;
const { getByText } = render( const { getByText } = render(
<BrowserRouter> <BrowserRouter>
<GroupCallView <TooltipProvider>
client={client} <GroupCallView
isPasswordlessUser={false} client={client}
confineToRoom={false} isPasswordlessUser={false}
preload={false} confineToRoom={false}
skipLobby={false} preload={false}
hideHeader={true} skipLobby={false}
rtcSession={rtcSession as unknown as MatrixRTCSession} hideHeader={true}
isJoined rtcSession={rtcSession as unknown as MatrixRTCSession}
muteStates={muteState} isJoined={joined}
widget={widget} muteStates={muteState}
/> widget={widget}
/>
</TooltipProvider>
</BrowserRouter>, </BrowserRouter>,
); );
return { return {
@@ -132,7 +159,7 @@ function createGroupCallView(widget: WidgetHelpers | null): {
}; };
} }
test("will play a leave sound asynchronously in SPA mode", async () => { test("GroupCallView plays a leave sound asynchronously in SPA mode", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const { getByText, rtcSession } = createGroupCallView(null); const { getByText, rtcSession } = createGroupCallView(null);
const leaveButton = getByText("Leave"); const leaveButton = getByText("Leave");
@@ -143,13 +170,13 @@ test("will play a leave sound asynchronously in SPA mode", async () => {
"user", "user",
expect.any(Promise), expect.any(Promise),
); );
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce(); expect(leaveRTCSession).toHaveBeenCalledOnce();
// Ensure that the playSound promise resolves within this test to avoid // Ensure that the playSound promise resolves within this test to avoid
// impacting the results of other tests // impacting the results of other tests
await waitFor(() => expect(leaveRTCSession).toHaveResolved()); await waitFor(() => expect(leaveRTCSession).toHaveResolved());
}); });
test("will play a leave sound synchronously in widget mode", async () => { test("GroupCallView plays a leave sound synchronously in widget mode", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const widget = { const widget = {
api: { api: {
@@ -158,7 +185,7 @@ test("will play a leave sound synchronously in widget mode", async () => {
lazyActions: new LazyEventEmitter(), lazyActions: new LazyEventEmitter(),
}; };
let resolvePlaySound: () => void; let resolvePlaySound: () => void;
playSound = vitest playSound = vi
.fn() .fn()
.mockReturnValue( .mockReturnValue(
new Promise<void>((resolve) => (resolvePlaySound = resolve)), new Promise<void>((resolve) => (resolvePlaySound = resolve)),
@@ -183,7 +210,7 @@ test("will play a leave sound synchronously in widget mode", async () => {
"user", "user",
expect.any(Promise), expect.any(Promise),
); );
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce(); expect(leaveRTCSession).toHaveBeenCalledOnce();
}); });
test("GroupCallView leaves the session when an error occurs", async () => { test("GroupCallView leaves the session when an error occurs", async () => {
@@ -205,8 +232,15 @@ test("GroupCallView leaves the session when an error occurs", async () => {
"error", "error",
expect.any(Promise), expect.any(Promise),
); );
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce(); });
// Ensure that the playSound promise resolves within this test to avoid
// impacting the results of other tests test("GroupCallView shows errors that occur during joining", async () => {
await waitFor(() => expect(leaveRTCSession).toHaveResolved()); const user = userEvent.setup();
enterRTCSession.mockRejectedValue(new MatrixRTCFocusMissingError(""));
onTestFinished(() => {
enterRTCSession.mockReset();
});
createGroupCallView(null, false);
await user.click(screen.getByRole("button", { name: "Join call" }));
screen.getByText("Call is not supported");
}); });

View File

@@ -67,7 +67,6 @@ import {
useSetting, useSetting,
} from "../settings/settings"; } from "../settings/settings";
import { useTypedEventEmitter } from "../useEvents"; import { useTypedEventEmitter } from "../useEvents";
import { useGroupCallErrorBoundary } from "./useCallErrorBoundary.ts";
declare global { declare global {
interface Window { interface Window {
@@ -100,6 +99,11 @@ export const GroupCallView: FC<Props> = ({
muteStates, muteStates,
widget, widget,
}) => { }) => {
// Used to thread through any errors that occur outside the error boundary
const [externalError, setExternalError] = useState<ElementCallError | null>(
null,
);
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const leaveSoundContext = useLatest( const leaveSoundContext = useLatest(
useAudioContext({ useAudioContext({
@@ -121,13 +125,11 @@ export const GroupCallView: FC<Props> = ({
}; };
}, [rtcSession]); }, [rtcSession]);
const { showGroupCallErrorBoundary } = useGroupCallErrorBoundary();
useTypedEventEmitter( useTypedEventEmitter(
rtcSession, rtcSession,
MatrixRTCSessionEvent.MembershipManagerError, MatrixRTCSessionEvent.MembershipManagerError,
(error) => { (error) => {
showGroupCallErrorBoundary( setExternalError(
new RTCSessionError( new RTCSessionError(
ErrorCode.MEMBERSHIP_MANAGER_UNRECOVERABLE, ErrorCode.MEMBERSHIP_MANAGER_UNRECOVERABLE,
error.message ?? error, error.message ?? error,
@@ -190,17 +192,17 @@ export const GroupCallView: FC<Props> = ({
); );
} catch (e) { } catch (e) {
if (e instanceof ElementCallError) { if (e instanceof ElementCallError) {
showGroupCallErrorBoundary(e); setExternalError(e);
} else { } else {
logger.error(`Unknown Error while entering RTC session`, e); logger.error(`Unknown Error while entering RTC session`, e);
const error = new UnknownCallError( const error = new UnknownCallError(
e instanceof Error ? e : new Error("Unknown error", { cause: e }), e instanceof Error ? e : new Error("Unknown error", { cause: e }),
); );
showGroupCallErrorBoundary(error); setExternalError(error);
} }
} }
}, },
[showGroupCallErrorBoundary], [setExternalError],
); );
useEffect(() => { useEffect(() => {
@@ -422,7 +424,15 @@ export const GroupCallView: FC<Props> = ({
); );
let body: ReactNode; let body: ReactNode;
if (isJoined) { if (externalError) {
// If an error was recorded within this component but outside
// GroupCallErrorBoundary, create a component that rethrows the error from
// within the error boundary, so it can be handled uniformly
const ErrorComponent = (): ReactNode => {
throw externalError;
};
body = <ErrorComponent />;
} else if (isJoined) {
body = ( body = (
<> <>
{shareModal} {shareModal}

View File

@@ -166,7 +166,6 @@ const widgetPostHangupProcedure = async (
logger.error("Failed to send close action", e); logger.error("Failed to send close action", e);
} }
widget.api.transport.stop(); widget.api.transport.stop();
PosthogAnalytics.instance.logout();
} }
}; };

View File

@@ -11,19 +11,19 @@ import { type ReactElement, useCallback } from "react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx"; import { GroupCallErrorBoundary } from "./room/GroupCallErrorBoundary";
import { useGroupCallErrorBoundary } from "./useCallErrorBoundary.ts"; import { useErrorBoundary } from "./useErrorBoundary";
import { ConnectionLostError } from "../utils/errors.ts"; import { ConnectionLostError } from "./utils/errors";
it("should show async error", async () => { it("should show async error", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const TestComponent = (): ReactElement => { const TestComponent = (): ReactElement => {
const { showGroupCallErrorBoundary } = useGroupCallErrorBoundary(); const { showErrorBoundary } = useErrorBoundary();
const onClick = useCallback((): void => { const onClick = useCallback((): void => {
showGroupCallErrorBoundary(new ConnectionLostError()); showErrorBoundary(new ConnectionLostError());
}, [showGroupCallErrorBoundary]); }, [showErrorBoundary]);
return ( return (
<div> <div>

View File

@@ -7,18 +7,16 @@ Please see LICENSE in the repository root for full details.
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import type { ElementCallError } from "../utils/errors.ts";
export type UseErrorBoundaryApi = { export type UseErrorBoundaryApi = {
showGroupCallErrorBoundary: (error: ElementCallError) => void; showErrorBoundary: (error: Error) => void;
}; };
export function useGroupCallErrorBoundary(): UseErrorBoundaryApi { export function useErrorBoundary(): UseErrorBoundaryApi {
const [error, setError] = useState<ElementCallError | null>(null); const [error, setError] = useState<Error | null>(null);
const memoized: UseErrorBoundaryApi = useMemo( const memoized: UseErrorBoundaryApi = useMemo(
() => ({ () => ({
showGroupCallErrorBoundary: (error: ElementCallError) => setError(error), showErrorBoundary: (error: Error) => setError(error),
}), }),
[], [],
); );

View File

@@ -286,8 +286,9 @@ export class MockRTCSession extends TypedEventEmitter<
super(); super();
} }
public isJoined(): true { public joined = true;
return true; public isJoined(): boolean {
return this.joined;
} }
public withMemberships( public withMemberships(

386
yarn.lock
View File

@@ -2000,177 +2000,177 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/aix-ppc64@npm:0.25.0": "@esbuild/aix-ppc64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/aix-ppc64@npm:0.25.0" resolution: "@esbuild/aix-ppc64@npm:0.25.1"
conditions: os=aix & cpu=ppc64 conditions: os=aix & cpu=ppc64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/android-arm64@npm:0.25.0": "@esbuild/android-arm64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/android-arm64@npm:0.25.0" resolution: "@esbuild/android-arm64@npm:0.25.1"
conditions: os=android & cpu=arm64 conditions: os=android & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/android-arm@npm:0.25.0": "@esbuild/android-arm@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/android-arm@npm:0.25.0" resolution: "@esbuild/android-arm@npm:0.25.1"
conditions: os=android & cpu=arm conditions: os=android & cpu=arm
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/android-x64@npm:0.25.0": "@esbuild/android-x64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/android-x64@npm:0.25.0" resolution: "@esbuild/android-x64@npm:0.25.1"
conditions: os=android & cpu=x64 conditions: os=android & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/darwin-arm64@npm:0.25.0": "@esbuild/darwin-arm64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/darwin-arm64@npm:0.25.0" resolution: "@esbuild/darwin-arm64@npm:0.25.1"
conditions: os=darwin & cpu=arm64 conditions: os=darwin & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/darwin-x64@npm:0.25.0": "@esbuild/darwin-x64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/darwin-x64@npm:0.25.0" resolution: "@esbuild/darwin-x64@npm:0.25.1"
conditions: os=darwin & cpu=x64 conditions: os=darwin & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/freebsd-arm64@npm:0.25.0": "@esbuild/freebsd-arm64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/freebsd-arm64@npm:0.25.0" resolution: "@esbuild/freebsd-arm64@npm:0.25.1"
conditions: os=freebsd & cpu=arm64 conditions: os=freebsd & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/freebsd-x64@npm:0.25.0": "@esbuild/freebsd-x64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/freebsd-x64@npm:0.25.0" resolution: "@esbuild/freebsd-x64@npm:0.25.1"
conditions: os=freebsd & cpu=x64 conditions: os=freebsd & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/linux-arm64@npm:0.25.0": "@esbuild/linux-arm64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/linux-arm64@npm:0.25.0" resolution: "@esbuild/linux-arm64@npm:0.25.1"
conditions: os=linux & cpu=arm64 conditions: os=linux & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/linux-arm@npm:0.25.0": "@esbuild/linux-arm@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/linux-arm@npm:0.25.0" resolution: "@esbuild/linux-arm@npm:0.25.1"
conditions: os=linux & cpu=arm conditions: os=linux & cpu=arm
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/linux-ia32@npm:0.25.0": "@esbuild/linux-ia32@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/linux-ia32@npm:0.25.0" resolution: "@esbuild/linux-ia32@npm:0.25.1"
conditions: os=linux & cpu=ia32 conditions: os=linux & cpu=ia32
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/linux-loong64@npm:0.25.0": "@esbuild/linux-loong64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/linux-loong64@npm:0.25.0" resolution: "@esbuild/linux-loong64@npm:0.25.1"
conditions: os=linux & cpu=loong64 conditions: os=linux & cpu=loong64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/linux-mips64el@npm:0.25.0": "@esbuild/linux-mips64el@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/linux-mips64el@npm:0.25.0" resolution: "@esbuild/linux-mips64el@npm:0.25.1"
conditions: os=linux & cpu=mips64el conditions: os=linux & cpu=mips64el
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/linux-ppc64@npm:0.25.0": "@esbuild/linux-ppc64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/linux-ppc64@npm:0.25.0" resolution: "@esbuild/linux-ppc64@npm:0.25.1"
conditions: os=linux & cpu=ppc64 conditions: os=linux & cpu=ppc64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/linux-riscv64@npm:0.25.0": "@esbuild/linux-riscv64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/linux-riscv64@npm:0.25.0" resolution: "@esbuild/linux-riscv64@npm:0.25.1"
conditions: os=linux & cpu=riscv64 conditions: os=linux & cpu=riscv64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/linux-s390x@npm:0.25.0": "@esbuild/linux-s390x@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/linux-s390x@npm:0.25.0" resolution: "@esbuild/linux-s390x@npm:0.25.1"
conditions: os=linux & cpu=s390x conditions: os=linux & cpu=s390x
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/linux-x64@npm:0.25.0": "@esbuild/linux-x64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/linux-x64@npm:0.25.0" resolution: "@esbuild/linux-x64@npm:0.25.1"
conditions: os=linux & cpu=x64 conditions: os=linux & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/netbsd-arm64@npm:0.25.0": "@esbuild/netbsd-arm64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/netbsd-arm64@npm:0.25.0" resolution: "@esbuild/netbsd-arm64@npm:0.25.1"
conditions: os=netbsd & cpu=arm64 conditions: os=netbsd & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/netbsd-x64@npm:0.25.0": "@esbuild/netbsd-x64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/netbsd-x64@npm:0.25.0" resolution: "@esbuild/netbsd-x64@npm:0.25.1"
conditions: os=netbsd & cpu=x64 conditions: os=netbsd & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/openbsd-arm64@npm:0.25.0": "@esbuild/openbsd-arm64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/openbsd-arm64@npm:0.25.0" resolution: "@esbuild/openbsd-arm64@npm:0.25.1"
conditions: os=openbsd & cpu=arm64 conditions: os=openbsd & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/openbsd-x64@npm:0.25.0": "@esbuild/openbsd-x64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/openbsd-x64@npm:0.25.0" resolution: "@esbuild/openbsd-x64@npm:0.25.1"
conditions: os=openbsd & cpu=x64 conditions: os=openbsd & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/sunos-x64@npm:0.25.0": "@esbuild/sunos-x64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/sunos-x64@npm:0.25.0" resolution: "@esbuild/sunos-x64@npm:0.25.1"
conditions: os=sunos & cpu=x64 conditions: os=sunos & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/win32-arm64@npm:0.25.0": "@esbuild/win32-arm64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/win32-arm64@npm:0.25.0" resolution: "@esbuild/win32-arm64@npm:0.25.1"
conditions: os=win32 & cpu=arm64 conditions: os=win32 & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/win32-ia32@npm:0.25.0": "@esbuild/win32-ia32@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/win32-ia32@npm:0.25.0" resolution: "@esbuild/win32-ia32@npm:0.25.1"
conditions: os=win32 & cpu=ia32 conditions: os=win32 & cpu=ia32
languageName: node languageName: node
linkType: hard linkType: hard
"@esbuild/win32-x64@npm:0.25.0": "@esbuild/win32-x64@npm:0.25.1":
version: 0.25.0 version: 0.25.1
resolution: "@esbuild/win32-x64@npm:0.25.0" resolution: "@esbuild/win32-x64@npm:0.25.1"
conditions: os=win32 & cpu=x64 conditions: os=win32 & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3998,135 +3998,142 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-android-arm-eabi@npm:4.34.9": "@rollup/rollup-android-arm-eabi@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-android-arm-eabi@npm:4.34.9" resolution: "@rollup/rollup-android-arm-eabi@npm:4.37.0"
conditions: os=android & cpu=arm conditions: os=android & cpu=arm
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-android-arm64@npm:4.34.9": "@rollup/rollup-android-arm64@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-android-arm64@npm:4.34.9" resolution: "@rollup/rollup-android-arm64@npm:4.37.0"
conditions: os=android & cpu=arm64 conditions: os=android & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-darwin-arm64@npm:4.34.9": "@rollup/rollup-darwin-arm64@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-darwin-arm64@npm:4.34.9" resolution: "@rollup/rollup-darwin-arm64@npm:4.37.0"
conditions: os=darwin & cpu=arm64 conditions: os=darwin & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-darwin-x64@npm:4.34.9": "@rollup/rollup-darwin-x64@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-darwin-x64@npm:4.34.9" resolution: "@rollup/rollup-darwin-x64@npm:4.37.0"
conditions: os=darwin & cpu=x64 conditions: os=darwin & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-freebsd-arm64@npm:4.34.9": "@rollup/rollup-freebsd-arm64@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-freebsd-arm64@npm:4.34.9" resolution: "@rollup/rollup-freebsd-arm64@npm:4.37.0"
conditions: os=freebsd & cpu=arm64 conditions: os=freebsd & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-freebsd-x64@npm:4.34.9": "@rollup/rollup-freebsd-x64@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-freebsd-x64@npm:4.34.9" resolution: "@rollup/rollup-freebsd-x64@npm:4.37.0"
conditions: os=freebsd & cpu=x64 conditions: os=freebsd & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-arm-gnueabihf@npm:4.34.9": "@rollup/rollup-linux-arm-gnueabihf@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.34.9" resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.37.0"
conditions: os=linux & cpu=arm & libc=glibc conditions: os=linux & cpu=arm & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-arm-musleabihf@npm:4.34.9": "@rollup/rollup-linux-arm-musleabihf@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.34.9" resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.37.0"
conditions: os=linux & cpu=arm & libc=musl conditions: os=linux & cpu=arm & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-arm64-gnu@npm:4.34.9": "@rollup/rollup-linux-arm64-gnu@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.34.9" resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.37.0"
conditions: os=linux & cpu=arm64 & libc=glibc conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-arm64-musl@npm:4.34.9": "@rollup/rollup-linux-arm64-musl@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-arm64-musl@npm:4.34.9" resolution: "@rollup/rollup-linux-arm64-musl@npm:4.37.0"
conditions: os=linux & cpu=arm64 & libc=musl conditions: os=linux & cpu=arm64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-loongarch64-gnu@npm:4.34.9": "@rollup/rollup-linux-loongarch64-gnu@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.34.9" resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.37.0"
conditions: os=linux & cpu=loong64 & libc=glibc conditions: os=linux & cpu=loong64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-powerpc64le-gnu@npm:4.34.9": "@rollup/rollup-linux-powerpc64le-gnu@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.34.9" resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.37.0"
conditions: os=linux & cpu=ppc64 & libc=glibc conditions: os=linux & cpu=ppc64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-riscv64-gnu@npm:4.34.9": "@rollup/rollup-linux-riscv64-gnu@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.34.9" resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.37.0"
conditions: os=linux & cpu=riscv64 & libc=glibc conditions: os=linux & cpu=riscv64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-s390x-gnu@npm:4.34.9": "@rollup/rollup-linux-riscv64-musl@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.34.9" resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.37.0"
conditions: os=linux & cpu=riscv64 & libc=musl
languageName: node
linkType: hard
"@rollup/rollup-linux-s390x-gnu@npm:4.37.0":
version: 4.37.0
resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.37.0"
conditions: os=linux & cpu=s390x & libc=glibc conditions: os=linux & cpu=s390x & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-x64-gnu@npm:4.34.9": "@rollup/rollup-linux-x64-gnu@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-x64-gnu@npm:4.34.9" resolution: "@rollup/rollup-linux-x64-gnu@npm:4.37.0"
conditions: os=linux & cpu=x64 & libc=glibc conditions: os=linux & cpu=x64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-linux-x64-musl@npm:4.34.9": "@rollup/rollup-linux-x64-musl@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-linux-x64-musl@npm:4.34.9" resolution: "@rollup/rollup-linux-x64-musl@npm:4.37.0"
conditions: os=linux & cpu=x64 & libc=musl conditions: os=linux & cpu=x64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-win32-arm64-msvc@npm:4.34.9": "@rollup/rollup-win32-arm64-msvc@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.34.9" resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.37.0"
conditions: os=win32 & cpu=arm64 conditions: os=win32 & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-win32-ia32-msvc@npm:4.34.9": "@rollup/rollup-win32-ia32-msvc@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.34.9" resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.37.0"
conditions: os=win32 & cpu=ia32 conditions: os=win32 & cpu=ia32
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/rollup-win32-x64-msvc@npm:4.34.9": "@rollup/rollup-win32-x64-msvc@npm:4.37.0":
version: 4.34.9 version: 4.37.0
resolution: "@rollup/rollup-win32-x64-msvc@npm:4.34.9" resolution: "@rollup/rollup-win32-x64-msvc@npm:4.37.0"
conditions: os=win32 & cpu=x64 conditions: os=win32 & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -7186,34 +7193,34 @@ __metadata:
linkType: hard linkType: hard
"esbuild@npm:^0.25.0": "esbuild@npm:^0.25.0":
version: 0.25.0 version: 0.25.1
resolution: "esbuild@npm:0.25.0" resolution: "esbuild@npm:0.25.1"
dependencies: dependencies:
"@esbuild/aix-ppc64": "npm:0.25.0" "@esbuild/aix-ppc64": "npm:0.25.1"
"@esbuild/android-arm": "npm:0.25.0" "@esbuild/android-arm": "npm:0.25.1"
"@esbuild/android-arm64": "npm:0.25.0" "@esbuild/android-arm64": "npm:0.25.1"
"@esbuild/android-x64": "npm:0.25.0" "@esbuild/android-x64": "npm:0.25.1"
"@esbuild/darwin-arm64": "npm:0.25.0" "@esbuild/darwin-arm64": "npm:0.25.1"
"@esbuild/darwin-x64": "npm:0.25.0" "@esbuild/darwin-x64": "npm:0.25.1"
"@esbuild/freebsd-arm64": "npm:0.25.0" "@esbuild/freebsd-arm64": "npm:0.25.1"
"@esbuild/freebsd-x64": "npm:0.25.0" "@esbuild/freebsd-x64": "npm:0.25.1"
"@esbuild/linux-arm": "npm:0.25.0" "@esbuild/linux-arm": "npm:0.25.1"
"@esbuild/linux-arm64": "npm:0.25.0" "@esbuild/linux-arm64": "npm:0.25.1"
"@esbuild/linux-ia32": "npm:0.25.0" "@esbuild/linux-ia32": "npm:0.25.1"
"@esbuild/linux-loong64": "npm:0.25.0" "@esbuild/linux-loong64": "npm:0.25.1"
"@esbuild/linux-mips64el": "npm:0.25.0" "@esbuild/linux-mips64el": "npm:0.25.1"
"@esbuild/linux-ppc64": "npm:0.25.0" "@esbuild/linux-ppc64": "npm:0.25.1"
"@esbuild/linux-riscv64": "npm:0.25.0" "@esbuild/linux-riscv64": "npm:0.25.1"
"@esbuild/linux-s390x": "npm:0.25.0" "@esbuild/linux-s390x": "npm:0.25.1"
"@esbuild/linux-x64": "npm:0.25.0" "@esbuild/linux-x64": "npm:0.25.1"
"@esbuild/netbsd-arm64": "npm:0.25.0" "@esbuild/netbsd-arm64": "npm:0.25.1"
"@esbuild/netbsd-x64": "npm:0.25.0" "@esbuild/netbsd-x64": "npm:0.25.1"
"@esbuild/openbsd-arm64": "npm:0.25.0" "@esbuild/openbsd-arm64": "npm:0.25.1"
"@esbuild/openbsd-x64": "npm:0.25.0" "@esbuild/openbsd-x64": "npm:0.25.1"
"@esbuild/sunos-x64": "npm:0.25.0" "@esbuild/sunos-x64": "npm:0.25.1"
"@esbuild/win32-arm64": "npm:0.25.0" "@esbuild/win32-arm64": "npm:0.25.1"
"@esbuild/win32-ia32": "npm:0.25.0" "@esbuild/win32-ia32": "npm:0.25.1"
"@esbuild/win32-x64": "npm:0.25.0" "@esbuild/win32-x64": "npm:0.25.1"
dependenciesMeta: dependenciesMeta:
"@esbuild/aix-ppc64": "@esbuild/aix-ppc64":
optional: true optional: true
@@ -7267,7 +7274,7 @@ __metadata:
optional: true optional: true
bin: bin:
esbuild: bin/esbuild esbuild: bin/esbuild
checksum: 10c0/5767b72da46da3cfec51661647ec850ddbf8a8d0662771139f10ef0692a8831396a0004b2be7966cecdb08264fb16bdc16290dcecd92396fac5f12d722fa013d checksum: 10c0/80fca30dd0f21aec23fdfab34f0a8d5f55df5097dd7f475f2ab561d45662c32ee306f5649071cd1a0ba0614b164c48ca3dc3ee1551a4daf204b8af90e4d893f5
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9737,11 +9744,11 @@ __metadata:
linkType: hard linkType: hard
"nanoid@npm:^3.3.8": "nanoid@npm:^3.3.8":
version: 3.3.8 version: 3.3.11
resolution: "nanoid@npm:3.3.8" resolution: "nanoid@npm:3.3.11"
bin: bin:
nanoid: bin/nanoid.cjs nanoid: bin/nanoid.cjs
checksum: 10c0/4b1bb29f6cfebf3be3bc4ad1f1296fb0a10a3043a79f34fbffe75d1621b4318319211cd420549459018ea3592f0d2f159247a6f874911d6d26eaaadda2478120 checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b
languageName: node languageName: node
linkType: hard linkType: hard
@@ -11483,28 +11490,29 @@ __metadata:
linkType: hard linkType: hard
"rollup@npm:^4.30.1": "rollup@npm:^4.30.1":
version: 4.34.9 version: 4.37.0
resolution: "rollup@npm:4.34.9" resolution: "rollup@npm:4.37.0"
dependencies: dependencies:
"@rollup/rollup-android-arm-eabi": "npm:4.34.9" "@rollup/rollup-android-arm-eabi": "npm:4.37.0"
"@rollup/rollup-android-arm64": "npm:4.34.9" "@rollup/rollup-android-arm64": "npm:4.37.0"
"@rollup/rollup-darwin-arm64": "npm:4.34.9" "@rollup/rollup-darwin-arm64": "npm:4.37.0"
"@rollup/rollup-darwin-x64": "npm:4.34.9" "@rollup/rollup-darwin-x64": "npm:4.37.0"
"@rollup/rollup-freebsd-arm64": "npm:4.34.9" "@rollup/rollup-freebsd-arm64": "npm:4.37.0"
"@rollup/rollup-freebsd-x64": "npm:4.34.9" "@rollup/rollup-freebsd-x64": "npm:4.37.0"
"@rollup/rollup-linux-arm-gnueabihf": "npm:4.34.9" "@rollup/rollup-linux-arm-gnueabihf": "npm:4.37.0"
"@rollup/rollup-linux-arm-musleabihf": "npm:4.34.9" "@rollup/rollup-linux-arm-musleabihf": "npm:4.37.0"
"@rollup/rollup-linux-arm64-gnu": "npm:4.34.9" "@rollup/rollup-linux-arm64-gnu": "npm:4.37.0"
"@rollup/rollup-linux-arm64-musl": "npm:4.34.9" "@rollup/rollup-linux-arm64-musl": "npm:4.37.0"
"@rollup/rollup-linux-loongarch64-gnu": "npm:4.34.9" "@rollup/rollup-linux-loongarch64-gnu": "npm:4.37.0"
"@rollup/rollup-linux-powerpc64le-gnu": "npm:4.34.9" "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.37.0"
"@rollup/rollup-linux-riscv64-gnu": "npm:4.34.9" "@rollup/rollup-linux-riscv64-gnu": "npm:4.37.0"
"@rollup/rollup-linux-s390x-gnu": "npm:4.34.9" "@rollup/rollup-linux-riscv64-musl": "npm:4.37.0"
"@rollup/rollup-linux-x64-gnu": "npm:4.34.9" "@rollup/rollup-linux-s390x-gnu": "npm:4.37.0"
"@rollup/rollup-linux-x64-musl": "npm:4.34.9" "@rollup/rollup-linux-x64-gnu": "npm:4.37.0"
"@rollup/rollup-win32-arm64-msvc": "npm:4.34.9" "@rollup/rollup-linux-x64-musl": "npm:4.37.0"
"@rollup/rollup-win32-ia32-msvc": "npm:4.34.9" "@rollup/rollup-win32-arm64-msvc": "npm:4.37.0"
"@rollup/rollup-win32-x64-msvc": "npm:4.34.9" "@rollup/rollup-win32-ia32-msvc": "npm:4.37.0"
"@rollup/rollup-win32-x64-msvc": "npm:4.37.0"
"@types/estree": "npm:1.0.6" "@types/estree": "npm:1.0.6"
fsevents: "npm:~2.3.2" fsevents: "npm:~2.3.2"
dependenciesMeta: dependenciesMeta:
@@ -11534,6 +11542,8 @@ __metadata:
optional: true optional: true
"@rollup/rollup-linux-riscv64-gnu": "@rollup/rollup-linux-riscv64-gnu":
optional: true optional: true
"@rollup/rollup-linux-riscv64-musl":
optional: true
"@rollup/rollup-linux-s390x-gnu": "@rollup/rollup-linux-s390x-gnu":
optional: true optional: true
"@rollup/rollup-linux-x64-gnu": "@rollup/rollup-linux-x64-gnu":
@@ -11550,7 +11560,7 @@ __metadata:
optional: true optional: true
bin: bin:
rollup: dist/bin/rollup rollup: dist/bin/rollup
checksum: 10c0/dd0be1f7c4f8a93040026be13ecc39259fb55313db0dac7eafd97a3ac01ab4584e6b1a8afd86b0259dcf391699d5560a678abe6c0729af0aa4f2d5df70f05c8c checksum: 10c0/2e00382e08938636edfe0a7547ea2eaa027205dc0b6ff85d8b82be0fbe55a4ef88a1995fee2a5059e33dbccf12d1376c236825353afb89c96298cc95c5160a46
languageName: node languageName: node
linkType: hard linkType: hard
@@ -13091,8 +13101,8 @@ __metadata:
linkType: hard linkType: hard
"vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.0.0": "vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.0.0":
version: 6.2.0 version: 6.2.3
resolution: "vite@npm:6.2.0" resolution: "vite@npm:6.2.3"
dependencies: dependencies:
esbuild: "npm:^0.25.0" esbuild: "npm:^0.25.0"
fsevents: "npm:~2.3.3" fsevents: "npm:~2.3.3"
@@ -13138,7 +13148,7 @@ __metadata:
optional: true optional: true
bin: bin:
vite: bin/vite.js vite: bin/vite.js
checksum: 10c0/db62c93d4a823e805c6f8429de035528b3c35cc7f6de4948b41e0528f94ed2ac55047d90f8534f626ef3a04e682883b570fe5ec9ee92f51bf0c3c210dbec5ac1 checksum: 10c0/ba6ad7e83e5a63fb0b6f62d3a3963624b8784bdc1bfa2a83e16cf268fb58c76bd9f8e69f39ed34bf8711cdb8fd7702916f878781da53c232c34ef7a85e0600cf
languageName: node languageName: node
linkType: hard linkType: hard