Compare commits

...

13 Commits

Author SHA1 Message Date
Hugh Nimmo-Smith
de2ecf009e Use type only import 2024-12-02 12:20:22 +00:00
Hugh Nimmo-Smith
28d49a42b7 Missing commit 2024-12-02 12:17:17 +00:00
Hugh Nimmo-Smith
ad2e012560 Lint 2024-12-02 11:59:38 +00:00
Hugh Nimmo-Smith
0395e5fdc9 Refactor the way we check for if widget or not to make it easier to mock 2024-12-02 11:59:20 +00:00
Hugh Nimmo-Smith
bc2281f7d2 Improve docs 2024-12-02 11:47:18 +00:00
Hugh Nimmo-Smith
c4ee55f9f2 Fix error message 2024-12-02 11:05:08 +00:00
Hugh Nimmo-Smith
e56baa6e5d Revert "Fix widget condition"
This reverts commit 9296accdc5.
2024-12-02 11:04:54 +00:00
Hugh Nimmo-Smith
9296accdc5 Fix widget condition 2024-12-02 11:03:29 +00:00
Hugh Nimmo-Smith
a5b9378d60 Remove unnecessary mock 2024-12-02 11:03:20 +00:00
Hugh Nimmo-Smith
9b3938cbe1 Merge branch 'toger5/allow-unmuted-join-in-widget' of https://github.com/element-hq/element-call into toger5/allow-unmuted-join-in-widget 2024-12-02 09:46:16 +00:00
Timo
cc85ed08a4 review 2024-11-29 16:52:29 +01:00
Hugh Nimmo-Smith
b53ab1fda5 Document change in URL params 2024-11-29 08:45:20 +00:00
Timo
20f3ced96e allow join unmuted in widget mode 2024-11-28 19:05:29 +01:00
8 changed files with 47 additions and 26 deletions

View File

@@ -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 users default homeserver. |

View File

@@ -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<string | null> {
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<void> {
if (!widget) {
if (!isRunningAsWidget) {
const client = window.matrixclient;
// the analytics ID only needs to be set in the standalone version.

View File

@@ -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(

View File

@@ -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(
<MemoryRouter initialEntries={["/room/?skipLobby=true"]}>
<MediaDevicesContext.Provider value={mockMediaDevices()}>
<TestComponent />
</MediaDevicesContext.Provider>
</MemoryRouter>,
);
expect(screen.getByTestId("audio-enabled").textContent).toBe("true");
expect(screen.getByTestId("video-enabled").textContent).toBe("true");
});
});

View File

@@ -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]);

View File

@@ -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.",

View File

@@ -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<Props> = ({
};
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);

View File

@@ -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;