diff --git a/docs/url-params.md b/docs/url-params.md index 010a4ec80..46c93b427 100644 --- a/docs/url-params.md +++ b/docs/url-params.md @@ -55,7 +55,7 @@ There are two formats for Element Call urls. | `returnToLobby` | `true` or `false` | No, defaults to `false` | Not applicable | Displays the lobby in widget mode after leaving a call; shows a blank page if set to `false`. Useful for video rooms. | | `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. | | `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. | -| `skipLobby` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) | +| `skipLobby` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with `preload` in widget. When `true` the audio and video inputs will be muted by default unless running as a widget. | | `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. | | `userId` | [Matrix User Identifier](https://spec.matrix.org/v1.12/appendices/#user-identifiers) | Yes | Not applicable | The Matrix user ID. | | `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. | diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 0df0ee320..40b0daef4 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -10,7 +10,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { Buffer } from "buffer"; -import { widget } from "../widget"; +import { isRunningAsWidget } from "../widget"; import { CallEndedTracker, CallStartedTracker, @@ -183,9 +183,9 @@ export class PosthogAnalytics { const appVersion = import.meta.env.VITE_APP_VERSION || "dev"; return { appVersion, - matrixBackend: widget ? "embedded" : "jssdk", + matrixBackend: isRunningAsWidget ? "embedded" : "jssdk", callBackend: "livekit", - cryptoVersion: widget + cryptoVersion: isRunningAsWidget ? undefined : window.matrixclient?.getCrypto()?.getVersion(), }; @@ -237,7 +237,7 @@ export class PosthogAnalytics { // different devices to send the same ID. let analyticsID = await this.getAnalyticsId(); try { - if (!analyticsID && !widget) { + if (!analyticsID && !isRunningAsWidget) { // only try setting up a new analytics ID in the standalone app. // Couldn't retrieve an analytics ID from user settings, so create one and set it on the server. @@ -269,7 +269,7 @@ export class PosthogAnalytics { private async getAnalyticsId(): Promise { const client: MatrixClient = window.matrixclient; let accountAnalyticsId; - if (widget) { + if (isRunningAsWidget) { accountAnalyticsId = getUrlParams().analyticsID; } else { const accountData = await client.getAccountDataFromServer( @@ -302,7 +302,7 @@ export class PosthogAnalytics { } private async setAccountAnalyticsId(analyticsID: string): Promise { - if (!widget) { + if (!isRunningAsWidget) { const client = window.matrixclient; // the analytics ID only needs to be set in the standalone version. diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index 2c272cb1f..96cba24d4 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -17,7 +17,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { initClient } from "../utils/matrix"; import { Session } from "../ClientContext"; import { Config } from "../config/Config"; -import { widget } from "../widget"; +import { isRunningAsWidget } from "../widget"; export const useInteractiveRegistration = ( oldClient?: MatrixClient, @@ -47,7 +47,7 @@ export const useInteractiveRegistration = ( } useEffect(() => { - if (widget) return; + if (isRunningAsWidget) return; // An empty registerRequest is used to get the privacy policy and recaptcha key. authClient.current!.registerRequest({}).catch((error) => { setPrivacyPolicyUrl( diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx index 6cc5815cc..3800fc42f 100644 --- a/src/room/MuteStates.test.tsx +++ b/src/room/MuteStates.test.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import React, { ReactNode } from "react"; import { beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; @@ -18,6 +18,7 @@ import { MediaDevicesContext, } from "../livekit/MediaDevicesContext"; import { mockConfig } from "../utils/test"; +import * as widget from "../widget"; function TestComponent(): ReactNode { const muteStates = useMuteStates(); @@ -98,10 +99,7 @@ describe("useMuteStates", () => { afterEach(() => { vi.restoreAllMocks(); - }); - - afterAll(() => { - vi.clearAllMocks(); + vi.unmock("../widget"); }); it("disabled when no input devices", () => { @@ -156,7 +154,7 @@ describe("useMuteStates", () => { expect(screen.getByTestId("video-enabled").textContent).toBe("false"); }); - it("skipLobby mutes inputs", () => { + it("skipLobby mutes inputs on SPA", () => { mockConfig(); render( @@ -169,4 +167,19 @@ describe("useMuteStates", () => { expect(screen.getByTestId("audio-enabled").textContent).toBe("false"); expect(screen.getByTestId("video-enabled").textContent).toBe("false"); }); + + it("skipLobby does not mute inputs in widget mode", () => { + mockConfig(); + vi.spyOn(widget, "isRunningAsWidget", "get").mockImplementation(() => true); + + render( + + + + + , + ); + expect(screen.getByTestId("audio-enabled").textContent).toBe("true"); + expect(screen.getByTestId("video-enabled").textContent).toBe("true"); + }); }); diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 1452c2501..1adc9250e 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -12,18 +12,18 @@ import { useEffect, useMemo, } from "react"; -import { IWidgetApiRequest } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; +import type { IWidgetApiRequest } from "matrix-widget-api"; import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext"; import { useReactiveState } from "../useReactiveState"; -import { ElementWidgetActions, widget } from "../widget"; +import { ElementWidgetActions, isRunningAsWidget, widget } from "../widget"; import { Config } from "../config/Config"; import { useUrlParams } from "../UrlParams"; /** * If there already are this many participants in the call, we automatically mute - * the user. + * the user when they join a call. */ export const MUTE_PARTICIPANT_COUNT = 8; @@ -74,13 +74,14 @@ export function useMuteStates(): MuteStates { const devices = useMediaDevices(); const { skipLobby } = useUrlParams(); - + // In SPA without lobby we need to protect from unmuted joins for privacy. + const allowStartUnmuted = !skipLobby || isRunningAsWidget; const audio = useMuteState(devices.audioInput, () => { - return Config.get().media_devices.enable_audio && !skipLobby; + return Config.get().media_devices.enable_audio && allowStartUnmuted; }); const video = useMuteState( devices.videoInput, - () => Config.get().media_devices.enable_video && !skipLobby, + () => Config.get().media_devices.enable_video && allowStartUnmuted, ); useEffect(() => { @@ -90,7 +91,7 @@ export function useMuteStates(): MuteStates { video_enabled: video.enabled, }) .catch((e) => - logger.warn("Could not send DeviceMute action to widget", e), + logger.warn("Could not send DeviceMute action to widget host", e), ); }, [audio, video]); diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 163571c8a..a68818e3f 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -20,7 +20,7 @@ import { KnownMembership } from "matrix-js-sdk/src/types"; import { JoinRule, MatrixError } from "matrix-js-sdk/src/matrix"; import { useTranslation } from "react-i18next"; -import { widget } from "../widget"; +import { isRunningAsWidget } from "../widget"; export type GroupCallLoaded = { kind: "loaded"; @@ -238,7 +238,7 @@ export const useLoadGroupCall = ( // room already joined so we are done here already. return room!; } - if (widget) + if (isRunningAsWidget) // in widget mode we never should reach this point. (getRoom should return the room.) throw new Error( "Room not found. The widget-api did not pass over the relevant room events/information.", diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 78afc2c5e..01478a728 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -21,7 +21,7 @@ import { useMediaDevices, useMediaDeviceNames, } from "../livekit/MediaDevicesContext"; -import { widget } from "../widget"; +import { isRunningAsWidget } from "../widget"; import { useSetting, developerSettingsTab as developerSettingsTabSetting, @@ -236,7 +236,7 @@ export const SettingsModal: FC = ({ }; const tabs = [audioTab, videoTab]; - if (widget === null) tabs.push(profileTab); + if (!isRunningAsWidget) tabs.push(profileTab); tabs.push(preferencesTab, feedbackTab, moreTab); if (developerSettingsTab) tabs.push(developerTab); diff --git a/src/widget.ts b/src/widget.ts index fb1b1cfdc..66f54fb67 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -181,3 +181,10 @@ export const widget = ((): WidgetHelpers | null => { return null; } })(); + +/** + * Whether or not we are running as a widget. + * + * @returns true if widget, false if SPA + */ +export const isRunningAsWidget: boolean = !!widget;