From 20f3ced96e72c25b09ebaf6ccfc70adfc75c5dd1 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 28 Nov 2024 19:05:29 +0100 Subject: [PATCH 01/12] allow join unmuted in widget mode --- src/room/MuteStates.test.tsx | 35 ++++++++++++++++++++++++++++++++++- src/room/MuteStates.ts | 7 ++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx index 6cc5815cc..22c1bce5f 100644 --- a/src/room/MuteStates.test.tsx +++ b/src/room/MuteStates.test.tsx @@ -156,8 +156,14 @@ describe("useMuteStates", () => { expect(screen.getByTestId("video-enabled").textContent).toBe("false"); }); - it("skipLobby mutes inputs", () => { + it("skipLobby mutes inputs on SPA", () => { mockConfig(); + vi.mock("../widget", () => { + return { + widget: null, + ElementWidgetActions: {}, + }; + }); render( @@ -169,4 +175,31 @@ 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.mock("../widget", () => { + return { + widget: { + api: { + transport: { + send: async (): Promise => new Promise((r) => r()), + }, + }, + lazyActions: { on: vi.fn(), off: vi.fn() }, + }, + ElementWidgetActions: {}, + }; + }); + + 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..e733314cc 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -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 (Privacy). + const allowStartUnmuted = !skipLobby || widget !== null; 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(() => { From b53ab1fda5a161772015849942ca7e58fc640bd6 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 29 Nov 2024 08:45:20 +0000 Subject: [PATCH 02/12] Document change in URL params --- docs/url-params.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. | From cc85ed08a422b4f9d55e32965df7819e956bfbc2 Mon Sep 17 00:00:00 2001 From: Timo Date: Fri, 29 Nov 2024 16:52:29 +0100 Subject: [PATCH 03/12] review --- src/room/MuteStates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index e733314cc..7d1061a24 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -75,7 +75,7 @@ export function useMuteStates(): MuteStates { const { skipLobby } = useUrlParams(); // In SPA without lobby we need to protect from unmuted joins (Privacy). - const allowStartUnmuted = !skipLobby || widget !== null; + const allowStartUnmuted = !skipLobby || !!widget; const audio = useMuteState(devices.audioInput, () => { return Config.get().media_devices.enable_audio && allowStartUnmuted; }); From a5b9378d60d91b3fbda5291b1a2fbacdfc268916 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Dec 2024 11:03:20 +0000 Subject: [PATCH 04/12] Remove unnecessary mock --- src/room/MuteStates.test.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx index 22c1bce5f..ba48997f1 100644 --- a/src/room/MuteStates.test.tsx +++ b/src/room/MuteStates.test.tsx @@ -158,12 +158,6 @@ describe("useMuteStates", () => { it("skipLobby mutes inputs on SPA", () => { mockConfig(); - vi.mock("../widget", () => { - return { - widget: null, - ElementWidgetActions: {}, - }; - }); render( From 9296accdc58d3690aa93aa0bc1c74334f0304fb8 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Dec 2024 11:03:29 +0000 Subject: [PATCH 05/12] Fix widget condition --- src/room/MuteStates.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 7d1061a24..25680d3f0 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -75,7 +75,7 @@ export function useMuteStates(): MuteStates { const { skipLobby } = useUrlParams(); // In SPA without lobby we need to protect from unmuted joins (Privacy). - const allowStartUnmuted = !skipLobby || !!widget; + const allowStartUnmuted = !skipLobby || !widget; const audio = useMuteState(devices.audioInput, () => { return Config.get().media_devices.enable_audio && allowStartUnmuted; }); @@ -91,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]); From e56baa6e5d847d66a1d669c88fe5964651c62db2 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Dec 2024 11:04:54 +0000 Subject: [PATCH 06/12] Revert "Fix widget condition" This reverts commit 9296accdc58d3690aa93aa0bc1c74334f0304fb8. --- src/room/MuteStates.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 25680d3f0..7d1061a24 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -75,7 +75,7 @@ export function useMuteStates(): MuteStates { const { skipLobby } = useUrlParams(); // In SPA without lobby we need to protect from unmuted joins (Privacy). - const allowStartUnmuted = !skipLobby || !widget; + const allowStartUnmuted = !skipLobby || !!widget; const audio = useMuteState(devices.audioInput, () => { return Config.get().media_devices.enable_audio && allowStartUnmuted; }); @@ -91,7 +91,7 @@ export function useMuteStates(): MuteStates { video_enabled: video.enabled, }) .catch((e) => - logger.warn("Could not send DeviceMute action to widget host", e), + logger.warn("Could not send DeviceMute action to widget", e), ); }, [audio, video]); From c4ee55f9f2a5f5840b79dbaa9da655ce22bfbb93 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Dec 2024 11:05:08 +0000 Subject: [PATCH 07/12] Fix error message --- src/room/MuteStates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 7d1061a24..6a8a18deb 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -91,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]); From bc2281f7d22e7cc284340d4267e544b920f6eaed Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Dec 2024 11:47:18 +0000 Subject: [PATCH 08/12] Improve docs --- src/room/MuteStates.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 6a8a18deb..02370b7be 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -23,7 +23,7 @@ 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,7 +74,7 @@ export function useMuteStates(): MuteStates { const devices = useMediaDevices(); const { skipLobby } = useUrlParams(); - // In SPA without lobby we need to protect from unmuted joins (Privacy). + // In SPA without lobby we need to protect from unmuted joins for privacy. const allowStartUnmuted = !skipLobby || !!widget; const audio = useMuteState(devices.audioInput, () => { return Config.get().media_devices.enable_audio && allowStartUnmuted; From 0395e5fdc973eb35a6658a910365c56e291bf2a0 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Dec 2024 11:59:20 +0000 Subject: [PATCH 09/12] Refactor the way we check for if widget or not to make it easier to mock --- src/analytics/PosthogAnalytics.ts | 4 ++-- src/auth/useInteractiveRegistration.ts | 4 ++-- src/room/MuteStates.test.tsx | 22 ++++------------------ src/room/MuteStates.ts | 4 ++-- src/room/useLoadGroupCall.ts | 4 ++-- src/settings/SettingsModal.tsx | 4 ++-- src/widget.ts | 7 +++++++ 7 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 0df0ee320..c93d04504 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, @@ -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( 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 ba48997f1..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", () => { @@ -172,19 +170,7 @@ describe("useMuteStates", () => { it("skipLobby does not mute inputs in widget mode", () => { mockConfig(); - vi.mock("../widget", () => { - return { - widget: { - api: { - transport: { - send: async (): Promise => new Promise((r) => r()), - }, - }, - lazyActions: { on: vi.fn(), off: vi.fn() }, - }, - ElementWidgetActions: {}, - }; - }); + vi.spyOn(widget, "isRunningAsWidget", "get").mockImplementation(() => true); render( diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 02370b7be..1ea47cc07 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -17,7 +17,7 @@ import { logger } from "matrix-js-sdk/src/logger"; 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"; @@ -75,7 +75,7 @@ export function useMuteStates(): MuteStates { const { skipLobby } = useUrlParams(); // In SPA without lobby we need to protect from unmuted joins for privacy. - const allowStartUnmuted = !skipLobby || !!widget; + const allowStartUnmuted = !skipLobby || isRunningAsWidget; const audio = useMuteState(devices.audioInput, () => { return Config.get().media_devices.enable_audio && allowStartUnmuted; }); 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..d05ac4e36 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; From ad2e012560e78a0c16e1353b831e181f75af0b8a Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Dec 2024 11:59:38 +0000 Subject: [PATCH 10/12] Lint --- src/widget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget.ts b/src/widget.ts index d05ac4e36..66f54fb67 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -184,7 +184,7 @@ export const widget = ((): WidgetHelpers | null => { /** * Whether or not we are running as a widget. - * + * * @returns true if widget, false if SPA */ export const isRunningAsWidget: boolean = !!widget; From 28d49a42b73b7fb08f9d61e4211b5041773207a6 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Dec 2024 12:17:17 +0000 Subject: [PATCH 11/12] Missing commit --- src/analytics/PosthogAnalytics.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index c93d04504..40b0daef4 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -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. @@ -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. From de2ecf009ec160b1cceaf9ade9c70f96bbc4b9cc Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 2 Dec 2024 12:20:22 +0000 Subject: [PATCH 12/12] Use type only import --- src/room/MuteStates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 1ea47cc07..1adc9250e 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -12,9 +12,9 @@ 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, isRunningAsWidget, widget } from "../widget";