mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
Stop reading URL parameters from inside the call path
The view models reached for getUrlParams() — and so window.location — from deep inside the call path: CallViewModel, MediaDevices, Publisher, LocalMember and the footer view model. An embedded Element Call has no URL of its own, so these values have to arrive as arguments instead. Add the relevant options to CallViewModelOptions, to the MediaDevices and Publisher constructors, to createLocalMembership$ and enterRTCSession, and to createCallFooterViewModel. The remaining React consumers read the context added in the previous commit. AppViewModel now takes its audio output options too, moving that URL read out to main.tsx, where the app shell can act as the adapter. The new CallViewModelOptions fields are optional, defaulting to what the URL parameters resolve to outside widget mode; the MediaDevices and Publisher arguments are required, so that every construction site has to be explicit. useTheme.test.ts mocked the UrlParams module with a factory, so it needed updating to mock the hook rather than getUrlParams. No functional change.
This commit is contained in:
+10
-3
@@ -116,7 +116,7 @@ export async function createMatrixRTCSdk(
|
||||
logger.info("client created");
|
||||
|
||||
// url params
|
||||
const { roomId } = getUrlParams();
|
||||
const { roomId, controlledAudioDevices, callIntent } = getUrlParams();
|
||||
if (roomId === null) throw Error("could not get roomId from url params");
|
||||
const room = client.getRoom(roomId);
|
||||
if (room === null) throw Error("could not get room from client");
|
||||
@@ -128,7 +128,10 @@ export async function createMatrixRTCSdk(
|
||||
const rtcSession = rtcSessionManager.getRoomSession(room);
|
||||
|
||||
// media devices
|
||||
const mediaDevices = new MediaDevices(scope);
|
||||
const mediaDevices = new MediaDevices(scope, {
|
||||
controlledAudioDevices,
|
||||
callIntent,
|
||||
});
|
||||
const muteStates = new MuteStates(scope, mediaDevices, {
|
||||
audioEnabled: false,
|
||||
videoEnabled: false,
|
||||
@@ -141,7 +144,11 @@ export async function createMatrixRTCSdk(
|
||||
room,
|
||||
mediaDevices,
|
||||
muteStates,
|
||||
{ encryptionSystem: { kind: E2eeType.PER_PARTICIPANT } },
|
||||
{
|
||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||
controlledAudioDevices,
|
||||
callIntent,
|
||||
},
|
||||
of({}),
|
||||
of({}),
|
||||
constant({ supported: false, processor: undefined }),
|
||||
|
||||
+8
-17
@@ -5,14 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
type FC,
|
||||
type JSX,
|
||||
Suspense,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { type FC, type JSX, Suspense, useEffect, useState } from "react";
|
||||
import { BrowserRouter, Route, useLocation, Routes } from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
@@ -32,7 +25,6 @@ import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
||||
import { type AppViewModel } from "./state/AppViewModel";
|
||||
import { MediaDevicesContext } from "./MediaDevicesContext";
|
||||
import {
|
||||
getUrlParams,
|
||||
HeaderStyle,
|
||||
UrlParamsProvider,
|
||||
useUrlParams,
|
||||
@@ -75,6 +67,12 @@ const ThemeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
/** Wraps the app in an {@link AppBar}, if the params ask for one. */
|
||||
const MaybeAppBar: FC<SimpleProviderProps> = ({ children }) => {
|
||||
const { header } = useUrlParams();
|
||||
return header === HeaderStyle.AppBar ? <AppBar>{children}</AppBar> : children;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
vm: AppViewModel;
|
||||
}
|
||||
@@ -91,9 +89,6 @@ export const App: FC<Props> = ({ vm }) => {
|
||||
.catch(logger.error);
|
||||
});
|
||||
|
||||
// Since we are outside the router component, we cannot use useUrlParams here
|
||||
const { header } = useMemo(getUrlParams, []);
|
||||
|
||||
const content = loaded ? (
|
||||
<ClientProvider>
|
||||
<MediaDevicesContext value={vm.mediaDevices}>
|
||||
@@ -123,11 +118,7 @@ export const App: FC<Props> = ({ vm }) => {
|
||||
<ThemeProvider>
|
||||
<TooltipProvider>
|
||||
<Suspense fallback={null}>
|
||||
{header === HeaderStyle.AppBar ? (
|
||||
<AppBar>{content}</AppBar>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
<MaybeAppBar>{content}</MaybeAppBar>
|
||||
</Suspense>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -29,7 +29,9 @@ const reactionData = {
|
||||
reactions$: new BehaviorSubject({}),
|
||||
};
|
||||
|
||||
const mediaDevices = new MediaDevices(globalScope);
|
||||
const mediaDevices = new MediaDevices(globalScope, {
|
||||
controlledAudioDevices: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* A wrapper component that is used for:
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { Alignment, Layout } from "../state/layout-types";
|
||||
import type { SpotlightTileViewModel } from "../state/TileViewModel";
|
||||
import type { DeviceLabel } from "../state/MediaDevices";
|
||||
import { createCallFooterViewModel } from "./CallFooterViewModel";
|
||||
import { HeaderStyle } from "../UrlParams";
|
||||
|
||||
const platformMock = vi.hoisted(() => vi.fn(() => "desktop"));
|
||||
vi.mock("../Platform", () => ({
|
||||
@@ -105,6 +106,7 @@ describe("createCallFooterViewModel", () => {
|
||||
mockMuteStates(),
|
||||
twoMicsAndOneCamMediaDevices,
|
||||
/* reactionIdentifier */ undefined,
|
||||
{ showControls: true, header: HeaderStyle.Standard },
|
||||
);
|
||||
|
||||
expect(vm.audioOptions$.value).toEqual([]);
|
||||
@@ -126,6 +128,7 @@ describe("createCallFooterViewModel", () => {
|
||||
mockMuteStates(),
|
||||
twoMicsAndOneCamMediaDevices,
|
||||
/* reactionIdentifier */ undefined,
|
||||
{ showControls: true, header: HeaderStyle.Standard },
|
||||
);
|
||||
|
||||
expect(vm.audioOptions$?.value).toEqual([
|
||||
|
||||
@@ -19,7 +19,7 @@ import { type Behavior, constant } from "../state/Behavior";
|
||||
import type { ObservableScope } from "../state/ObservableScope";
|
||||
import { type MuteStates } from "../state/MuteStates";
|
||||
import { createStaticViewModel, type ViewModel } from "../state/ViewModel";
|
||||
import { getUrlParams, HeaderStyle } from "../UrlParams";
|
||||
import { HeaderStyle } from "../UrlParams";
|
||||
import { platform } from "../Platform";
|
||||
import { type FooterSnapshot } from "./CallFooter";
|
||||
|
||||
@@ -138,6 +138,8 @@ function buildDeviceBehaviors(
|
||||
* @param mediaDevices - Available and selected input devices.
|
||||
* @param reactionIdentifier - The local user's reaction identifier string, or
|
||||
* undefined when reactions are not supported (hides the reaction button).
|
||||
* @param options - `showControls`: whether the call controls should be shown.
|
||||
* `header`: the style of header, which decides whether to show the logo.
|
||||
*/
|
||||
export function createCallFooterViewModel(
|
||||
scope: ObservableScope,
|
||||
@@ -145,8 +147,9 @@ export function createCallFooterViewModel(
|
||||
muteStates: MuteStates,
|
||||
mediaDevices: MediaDevices,
|
||||
reactionIdentifier: string | undefined,
|
||||
options: { showControls: boolean; header: HeaderStyle },
|
||||
): ViewModel<FooterSnapshot> {
|
||||
const { showControls, header: headerStyle } = getUrlParams();
|
||||
const { showControls, header: headerStyle } = options;
|
||||
const showLogo = headerStyle === HeaderStyle.Standard;
|
||||
|
||||
const isPip$ = scope.behavior(
|
||||
|
||||
@@ -14,7 +14,9 @@ import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||
import { MediaDevices } from "../state/MediaDevices";
|
||||
import { globalScope } from "../state/ObservableScope";
|
||||
|
||||
const mediaDevices = new MediaDevices(globalScope);
|
||||
const mediaDevices = new MediaDevices(globalScope, {
|
||||
controlledAudioDevices: false,
|
||||
});
|
||||
|
||||
const meta = {
|
||||
component: MediaMuteAndSwitchButton,
|
||||
|
||||
+10
-1
@@ -21,6 +21,7 @@ import { init as initRageshake } from "./settings/rageshake";
|
||||
import { Initializer } from "./initializer";
|
||||
import { AppViewModel } from "./state/AppViewModel";
|
||||
import { globalScope } from "./state/ObservableScope";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
|
||||
initRageshake().catch((e) => {
|
||||
logger.error("Failed to initialize rageshake", e);
|
||||
@@ -49,9 +50,17 @@ if (fatalError !== null) {
|
||||
|
||||
Initializer.initBeforeReact()
|
||||
.then(() => {
|
||||
const { controlledAudioDevices, callIntent } = getUrlParams();
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App vm={new AppViewModel(globalScope)} />
|
||||
<App
|
||||
vm={
|
||||
new AppViewModel(globalScope, {
|
||||
controlledAudioDevices,
|
||||
callIntent,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</StrictMode>,
|
||||
);
|
||||
})
|
||||
|
||||
@@ -54,12 +54,7 @@ import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useRoomName } from "./useRoomName";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
import {
|
||||
getUrlParams,
|
||||
HeaderStyle,
|
||||
type UrlParams,
|
||||
useUrlParams,
|
||||
} from "../UrlParams";
|
||||
import { HeaderStyle, type UrlParams, useUrlParams } from "../UrlParams";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import {
|
||||
@@ -406,7 +401,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
}
|
||||
// On a normal user hangup we can shut down and close the widget. But if an
|
||||
// error occurs we should keep the widget open until the user reads it.
|
||||
if (reason != "error" && !getUrlParams().returnToLobby) {
|
||||
if (reason != "error" && !returnToLobby) {
|
||||
try {
|
||||
await widget.api.transport.send(ElementWidgetActions.Close, {});
|
||||
} catch (e) {
|
||||
@@ -425,6 +420,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
rtcSession,
|
||||
isPasswordlessUser,
|
||||
confineToRoom,
|
||||
returnToLobby,
|
||||
navigate,
|
||||
],
|
||||
);
|
||||
|
||||
+17
-2
@@ -122,8 +122,16 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
rootLogger.info("START CALL VIEW SCOPE");
|
||||
const scope = new ObservableScope();
|
||||
const reactionsReader = new ReactionsReader(scope, props.rtcSession);
|
||||
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
|
||||
urlParams;
|
||||
const {
|
||||
autoLeaveWhenOthersLeft,
|
||||
waitForCallPickup,
|
||||
sendNotificationType,
|
||||
controlledAudioDevices,
|
||||
header,
|
||||
showControls,
|
||||
hideScreensharing,
|
||||
callIntent,
|
||||
} = urlParams;
|
||||
|
||||
const vm = createCallViewModel$(
|
||||
scope,
|
||||
@@ -136,6 +144,12 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
autoLeaveWhenOthersLeft,
|
||||
waitForCallPickup: waitForCallPickup && sendNotificationType === "ring",
|
||||
matrixRTCMode$: matrixRTCModeSetting.value$,
|
||||
controlledAudioDevices,
|
||||
header,
|
||||
showControls,
|
||||
hideScreensharing,
|
||||
sendNotificationType,
|
||||
callIntent,
|
||||
},
|
||||
reactionsReader.raisedHands$,
|
||||
reactionsReader.reactions$,
|
||||
@@ -172,6 +186,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
props.muteStates,
|
||||
mediaDevices,
|
||||
`${props.client.getUserId()}:${props.client.getDeviceId()}`,
|
||||
{ showControls: urlParams.showControls, header: urlParams.header },
|
||||
);
|
||||
setFooterVm(footerVm);
|
||||
setDeveloperSettingsVm(createDeveloperSettingsTabViewModel(scope, vm));
|
||||
|
||||
@@ -5,17 +5,23 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MediaDevices } from "./MediaDevices";
|
||||
import { type AudioOutputOptions, MediaDevices } from "./MediaDevices";
|
||||
import { type ObservableScope } from "./ObservableScope";
|
||||
|
||||
/**
|
||||
* The top-level state holder for the application.
|
||||
*/
|
||||
export class AppViewModel {
|
||||
public readonly mediaDevices = new MediaDevices(this.scope);
|
||||
public readonly mediaDevices = new MediaDevices(
|
||||
this.scope,
|
||||
this.audioOutputOptions,
|
||||
);
|
||||
|
||||
// TODO: Move more application logic here. The CallViewModel, at the very
|
||||
// least, ought to be accessible from this object.
|
||||
|
||||
public constructor(private readonly scope: ObservableScope) {}
|
||||
public constructor(
|
||||
private readonly scope: ObservableScope,
|
||||
private readonly audioOutputOptions: AudioOutputOptions,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1593,12 +1593,13 @@ describe.each(modes)("CallViewModel (%s mode)", (mode) => {
|
||||
|
||||
it.skip("audio output changes when toggling earpiece mode", () => {
|
||||
withTestScheduler(({ schedule, expectObservable }) => {
|
||||
getUrlParams.mockReturnValue({ controlledAudioDevices: true });
|
||||
vi.mocked(ComponentsCore.createMediaDeviceObserver).mockReturnValue(
|
||||
of([]),
|
||||
);
|
||||
|
||||
const devices = new MediaDevices(testScope());
|
||||
const devices = new MediaDevices(testScope(), {
|
||||
controlledAudioDevices: true,
|
||||
});
|
||||
|
||||
window.controls.setAvailableAudioDevices([
|
||||
{ id: "speaker", name: "Speaker", isSpeaker: true },
|
||||
|
||||
@@ -45,6 +45,8 @@ import {
|
||||
MembershipManagerEvent,
|
||||
type LivekitTransportConfig,
|
||||
type MatrixRTCSession,
|
||||
type RTCCallIntent,
|
||||
type RTCNotificationType,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { type IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||
@@ -85,7 +87,7 @@ import { constant, type Behavior } from "../Behavior";
|
||||
import { E2eeType } from "../../e2ee/e2eeType";
|
||||
import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider";
|
||||
import { type MuteStates } from "../MuteStates";
|
||||
import { getUrlParams, HeaderStyle } from "../../UrlParams";
|
||||
import { HeaderStyle } from "../../UrlParams";
|
||||
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
||||
import { ElementWidgetActions, widget } from "../../widget";
|
||||
import {
|
||||
@@ -171,6 +173,23 @@ import { type GridTileViewModel } from "../TileViewModel.ts";
|
||||
// callMembership -> rtcMembership
|
||||
export interface CallViewModelOptions {
|
||||
encryptionSystem: EncryptionSystem;
|
||||
/**
|
||||
* Whether the app hosting Element Call controls the audio output devices,
|
||||
* rather than the browser. Defaults to false.
|
||||
*/
|
||||
controlledAudioDevices?: boolean;
|
||||
/** The style of header to show. Defaults to {@link HeaderStyle.Standard}. */
|
||||
header?: HeaderStyle;
|
||||
/** Whether the call controls should be shown. Defaults to true. */
|
||||
showControls?: boolean;
|
||||
/** Whether to hide the screen-sharing button. Defaults to false. */
|
||||
hideScreensharing?: boolean;
|
||||
/**
|
||||
* Whether and what kind of notification to send when joining the call.
|
||||
*/
|
||||
sendNotificationType?: RTCNotificationType;
|
||||
/** The kind of call being placed. */
|
||||
callIntent?: RTCCallIntent;
|
||||
autoLeaveWhenOthersLeft?: boolean;
|
||||
/**
|
||||
* If the call is started in a way where we want it to behave like a telephone usecase
|
||||
@@ -435,6 +454,17 @@ export function createCallViewModel$(
|
||||
if (!(userId && deviceId))
|
||||
throw new UnknownCallError(new Error("userId and deviceId are required"));
|
||||
|
||||
// Defaults match what the URL parameters resolve to outside of widget mode,
|
||||
// so that callers which don't care (chiefly tests) behave as they always have.
|
||||
const {
|
||||
controlledAudioDevices = false,
|
||||
header = HeaderStyle.Standard,
|
||||
showControls = true,
|
||||
hideScreensharing = false,
|
||||
sendNotificationType,
|
||||
callIntent,
|
||||
} = options;
|
||||
|
||||
const livekitKeyProvider = getE2eeKeyProvider(
|
||||
options.encryptionSystem,
|
||||
matrixRTCSession,
|
||||
@@ -523,7 +553,7 @@ export function createCallViewModel$(
|
||||
mediaDevices,
|
||||
trackProcessorState$,
|
||||
livekitKeyProvider,
|
||||
getUrlParams().controlledAudioDevices,
|
||||
controlledAudioDevices,
|
||||
options.livekitRoomFactory,
|
||||
);
|
||||
|
||||
@@ -563,6 +593,8 @@ export function createCallViewModel$(
|
||||
encryptMedia: livekitKeyProvider !== undefined,
|
||||
// TODO. This might need to get called again on each change of matrixRTCMode...
|
||||
matrixRTCMode: mode,
|
||||
sendNotificationType,
|
||||
callIntent,
|
||||
})),
|
||||
),
|
||||
);
|
||||
@@ -592,12 +624,14 @@ export function createCallViewModel$(
|
||||
logger.getChild(
|
||||
"[Publisher " + connection.transport.livekit_service_url + "]",
|
||||
),
|
||||
controlledAudioDevices,
|
||||
);
|
||||
},
|
||||
connectionManager,
|
||||
matrixRTCSession,
|
||||
localTransport$,
|
||||
roomId: matrixRoom.roomId,
|
||||
hideScreensharing,
|
||||
logger: logger.getChild(`[${Date.now()}]`),
|
||||
});
|
||||
|
||||
@@ -1457,9 +1491,8 @@ export function createCallViewModel$(
|
||||
),
|
||||
);
|
||||
|
||||
const urlParams = getUrlParams();
|
||||
const showFooterUrlParams = !(
|
||||
urlParams.header === HeaderStyle.None && urlParams.showControls === false
|
||||
header === HeaderStyle.None && showControls === false
|
||||
);
|
||||
const showFooter$ = scope.behavior(
|
||||
naturallyShowFooter$.pipe(
|
||||
@@ -1778,8 +1811,7 @@ export function createCallViewModel$(
|
||||
return {
|
||||
autoLeave$: autoLeave$,
|
||||
ringingVm$: ringingMedia$,
|
||||
ringingStatusLocation:
|
||||
urlParams.header === HeaderStyle.AppBar ? "app_bar" : "tile",
|
||||
ringingStatusLocation: header === HeaderStyle.AppBar ? "app_bar" : "tile",
|
||||
leave$: leave$,
|
||||
hangup: (): void => userHangup$.next(),
|
||||
join: localMembership.requestJoinAndPublish,
|
||||
|
||||
@@ -199,6 +199,7 @@ describe("LocalMembership", () => {
|
||||
rtsSession$: constant(RTCMemberStatus.Connected),
|
||||
},
|
||||
roomId: "!test-room-id:example.org",
|
||||
hideScreensharing: false,
|
||||
};
|
||||
|
||||
it("throws error on missing RTC config error", () => {
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
type LivekitTransport,
|
||||
type LivekitTransportConfig,
|
||||
type MatrixRTCSession,
|
||||
type RTCCallIntent,
|
||||
type RTCNotificationType,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
@@ -52,7 +54,7 @@ import {
|
||||
UnknownCallError,
|
||||
} from "../../../utils/errors.ts";
|
||||
import { ElementWidgetActions, widget } from "../../../widget.ts";
|
||||
import { getUrlParams } from "../../../UrlParams.ts";
|
||||
|
||||
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts";
|
||||
import {
|
||||
advancedScreenShare,
|
||||
@@ -141,6 +143,8 @@ interface Props {
|
||||
MatrixRTCSession,
|
||||
"updateCallIntent" | "leaveRoomSession"
|
||||
>;
|
||||
/** Whether to hide the screen-sharing button. */
|
||||
hideScreensharing: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
@@ -160,6 +164,7 @@ interface Props {
|
||||
* @param props.muteStates The mute states for video and audio.
|
||||
* @param props.matrixRTCSession The matrix RTC session to join.
|
||||
* @param props.roomId The room ID used as the call identifier in analytics events.
|
||||
* @param props.hideScreensharing Whether to hide the screen-sharing button.
|
||||
* @returns
|
||||
* - publisher: The handle to create tracks and publish them to the room.
|
||||
* - connected$: the current connection state. Including matrix server and livekit server connection. (only considering the livekit server we are using for our own media publication)
|
||||
@@ -178,6 +183,7 @@ export const createLocalMembership$ = ({
|
||||
muteStates,
|
||||
matrixRTCSession,
|
||||
roomId,
|
||||
hideScreensharing,
|
||||
}: Props): {
|
||||
/**
|
||||
* This request to start audio and video tracks.
|
||||
@@ -709,7 +715,7 @@ export const createLocalMembership$ = ({
|
||||
let toggleScreenSharing: (() => void) | null = null;
|
||||
if (
|
||||
"getDisplayMedia" in (navigator.mediaDevices ?? {}) &&
|
||||
!getUrlParams().hideScreensharing
|
||||
!hideScreensharing
|
||||
) {
|
||||
toggleScreenSharing = (): void => {
|
||||
const screenshareSettings: ScreenShareCaptureOptions = {
|
||||
@@ -820,6 +826,10 @@ export function observeSharingScreen$(p: Participant): Observable<boolean> {
|
||||
interface EnterRTCSessionOptions {
|
||||
encryptMedia: boolean;
|
||||
matrixRTCMode: MatrixRTCMode;
|
||||
/** Whether and what kind of notification to send when joining. */
|
||||
sendNotificationType?: RTCNotificationType;
|
||||
/** The kind of call being placed. */
|
||||
callIntent?: RTCCallIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -832,7 +842,9 @@ interface EnterRTCSessionOptions {
|
||||
* @param rtcSession - The MatrixRTCSession to join.
|
||||
* @param ownMembershipIdentity - Options for entering the RTC session.
|
||||
* @param transport - The LivekitTransport to use for this session.
|
||||
* @param options - `encryptMedia`: Whether to encrypt media `matrixRTCMode`: The Matrix RTC mode to use.
|
||||
* @param options - `encryptMedia`: Whether to encrypt media. `matrixRTCMode`: The
|
||||
* Matrix RTC mode to use. `sendNotificationType`: Whether and what kind of
|
||||
* notification to send on join. `callIntent`: The kind of call being placed.
|
||||
* @throws If the widget could not send ElementWidgetActions.JoinCall action.
|
||||
*/
|
||||
// Exported for unit testing
|
||||
@@ -842,7 +854,12 @@ export function enterRTCSession(
|
||||
transport: LivekitTransportConfig,
|
||||
options: EnterRTCSessionOptions,
|
||||
): void {
|
||||
const { encryptMedia, matrixRTCMode } = options;
|
||||
const {
|
||||
encryptMedia,
|
||||
matrixRTCMode,
|
||||
sendNotificationType: notificationType,
|
||||
callIntent,
|
||||
} = options;
|
||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
||||
|
||||
@@ -851,7 +868,6 @@ export function enterRTCSession(
|
||||
// groupCallOTelMembership?.onJoinCall();
|
||||
|
||||
const { matrix_rtc_session: matrixRtcSessionConfig } = Config.get();
|
||||
const { sendNotificationType: notificationType, callIntent } = getUrlParams();
|
||||
const multiSFU =
|
||||
matrixRTCMode === MatrixRTCMode.Compatibility ||
|
||||
matrixRTCMode === MatrixRTCMode.Matrix_2_0;
|
||||
|
||||
@@ -192,6 +192,7 @@ describe("Publisher", () => {
|
||||
muteStates,
|
||||
constant({ supported: false, processor: undefined }),
|
||||
logger,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -309,6 +310,7 @@ describe("Publisher", () => {
|
||||
muteStates,
|
||||
constant({ supported: false, processor: undefined }),
|
||||
logger,
|
||||
false,
|
||||
);
|
||||
});
|
||||
afterEach(async () => {
|
||||
@@ -364,6 +366,7 @@ describe("Bug fix", () => {
|
||||
muteStates,
|
||||
constant({ supported: false, processor: undefined }),
|
||||
logger,
|
||||
false,
|
||||
);
|
||||
audioEnabled$.next(true);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
type ProcessorState,
|
||||
trackProcessorSync,
|
||||
} from "../../../livekit/TrackProcessorContext.tsx";
|
||||
import { getUrlParams } from "../../../UrlParams.ts";
|
||||
|
||||
import { observeTrackReference$ } from "../../observeTrackReference";
|
||||
import { type Connection } from "../remoteMembers/Connection.ts";
|
||||
import { ObservableScope } from "../../ObservableScope.ts";
|
||||
@@ -56,6 +56,8 @@ export class Publisher {
|
||||
* @param muteStates - The mute states for audio and video.
|
||||
* @param trackerProcessorState$ - The processor state for the video track processor (e.g. background blur).
|
||||
* @param logger - The logger to use for logging :D.
|
||||
* @param controlledAudioDevices - Whether the app hosting Element Call
|
||||
* controls the audio output devices, rather than the browser.
|
||||
*/
|
||||
public constructor(
|
||||
private connection: Pick<Connection, "livekitRoom" | "state$">, //setE2EEEnabled,
|
||||
@@ -63,8 +65,8 @@ export class Publisher {
|
||||
private readonly muteStates: MuteStates,
|
||||
trackerProcessorState$: Behavior<ProcessorState>,
|
||||
private logger: Logger,
|
||||
controlledAudioDevices: boolean,
|
||||
) {
|
||||
const { controlledAudioDevices } = getUrlParams();
|
||||
const room = connection.livekitRoom;
|
||||
|
||||
room.setE2EEEnabled(room.options.e2ee !== undefined)?.catch((e: Error) => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "rxjs";
|
||||
import { createMediaDeviceObserver } from "@livekit/components-core";
|
||||
import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
import { type RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc";
|
||||
|
||||
import {
|
||||
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
} from "../settings/settings";
|
||||
import { type ObservableScope } from "./ObservableScope";
|
||||
import { availableOutputDevices$ as controlledAvailableOutputDevices$ } from "../controls";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import { platform } from "../Platform";
|
||||
import { switchWhen } from "../utils/observable";
|
||||
import { type Behavior, constant } from "./Behavior";
|
||||
@@ -338,6 +338,22 @@ class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How Element Call should manage audio output.
|
||||
*/
|
||||
export interface AudioOutputOptions {
|
||||
/**
|
||||
* Whether the list of output devices is controlled by the app hosting Element
|
||||
* Call, through the global JS controls, rather than by the browser.
|
||||
*/
|
||||
controlledAudioDevices: boolean;
|
||||
/**
|
||||
* The kind of call being placed, which decides the initial output route when
|
||||
* the host controls the devices.
|
||||
*/
|
||||
callIntent?: RTCCallIntent;
|
||||
}
|
||||
|
||||
export class MediaDevices {
|
||||
private readonly deviceNamesRequest$ = new Subject<void>();
|
||||
/**
|
||||
@@ -368,23 +384,28 @@ export class MediaDevices {
|
||||
public readonly audioOutput: MediaDevice<
|
||||
AudioOutputDeviceLabel,
|
||||
SelectedAudioOutputDevice
|
||||
> = getUrlParams().controlledAudioDevices
|
||||
> = this.audioOutputOptions.controlledAudioDevices
|
||||
? platform == "android"
|
||||
? new AndroidControlledAudioOutput(
|
||||
controlledAvailableOutputDevices$,
|
||||
this.scope,
|
||||
getUrlParams().callIntent,
|
||||
this.audioOutputOptions.callIntent,
|
||||
window.controls,
|
||||
)
|
||||
: new IOSControlledAudioOutput(
|
||||
this.usingNames$,
|
||||
this.scope,
|
||||
getUrlParams().callIntent,
|
||||
this.audioOutputOptions.callIntent,
|
||||
)
|
||||
: new AudioOutput(this.usingNames$, this.scope);
|
||||
|
||||
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
||||
new VideoInput(this.usingNames$, this.scope);
|
||||
|
||||
public constructor(private readonly scope: ObservableScope) {}
|
||||
// Note: both parameters are read by the field initializers above, which is
|
||||
// safe because TypeScript assigns parameter properties before running them.
|
||||
public constructor(
|
||||
private readonly scope: ObservableScope,
|
||||
private readonly audioOutputOptions: AudioOutputOptions,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ import EventEmitter from "events";
|
||||
import { WidgetApiToWidgetAction } from "matrix-widget-api";
|
||||
|
||||
import { useTheme } from "./useTheme";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { useUrlParams } from "./UrlParams";
|
||||
import { widget } from "./widget";
|
||||
|
||||
vi.mock("./UrlParams", () => ({ getUrlParams: vi.fn() }));
|
||||
vi.mock("./UrlParams", () => ({ useUrlParams: vi.fn() }));
|
||||
vi.mock("./widget", () => ({
|
||||
widget: {
|
||||
api: { transport: { reply: vi.fn() } },
|
||||
@@ -39,7 +39,7 @@ describe("useTheme", () => {
|
||||
vi.spyOn(originalClassList, "add");
|
||||
vi.spyOn(originalClassList, "remove");
|
||||
vi.spyOn(originalClassList, "item").mockReturnValue(null);
|
||||
(getUrlParams as Mock).mockReturnValue({ theme: "dark" });
|
||||
(useUrlParams as Mock).mockReturnValue({ theme: "dark" });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -53,7 +53,7 @@ describe("useTheme", () => {
|
||||
{ setTheme: "light-high-contrast", add: ["cpd-theme-light-hc"] },
|
||||
])("apply procedure", ({ setTheme, add }) => {
|
||||
test(`should apply ${add[0]} theme when ${setTheme} theme is specified`, () => {
|
||||
(getUrlParams as Mock).mockReturnValue({ theme: setTheme });
|
||||
(useUrlParams as Mock).mockReturnValue({ theme: setTheme });
|
||||
|
||||
renderHook(() => useTheme());
|
||||
|
||||
|
||||
+3
-4
@@ -9,15 +9,14 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { WidgetApiToWidgetAction } from "matrix-widget-api";
|
||||
import { type IThemeChangeActionRequest } from "matrix-widget-api";
|
||||
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { useUrlParams } from "./UrlParams";
|
||||
import { widget } from "./widget";
|
||||
import { useRootElement } from "./RootElementContext";
|
||||
|
||||
export const useTheme = (): void => {
|
||||
const rootElement = useRootElement();
|
||||
const [requestedTheme, setRequestedTheme] = useState(
|
||||
() => getUrlParams().theme,
|
||||
);
|
||||
const { theme } = useUrlParams();
|
||||
const [requestedTheme, setRequestedTheme] = useState(theme);
|
||||
const previousTheme = useRef<string | null>(rootElement.classList.item(0));
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -40,6 +40,7 @@ import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
||||
import { constant } from "../state/Behavior";
|
||||
import { MatrixRTCMode } from "../config/ConfigOptions";
|
||||
import { createCallFooterViewModel } from "../components/CallFooterViewModel";
|
||||
import { HeaderStyle } from "../UrlParams";
|
||||
import { type FooterSnapshot } from "../components/CallFooter";
|
||||
import { type ViewModel } from "../state/ViewModel";
|
||||
import { createDeveloperSettingsTabViewModel } from "../settings/DeveloperSettingsTabViewModel";
|
||||
@@ -187,6 +188,7 @@ export function getBasicCallViewModelEnvironment(
|
||||
muteStates,
|
||||
mediaDevices,
|
||||
"reactionId",
|
||||
{ showControls: true, header: HeaderStyle.Standard },
|
||||
);
|
||||
return {
|
||||
vm,
|
||||
|
||||
Reference in New Issue
Block a user