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");
|
logger.info("client created");
|
||||||
|
|
||||||
// url params
|
// url params
|
||||||
const { roomId } = getUrlParams();
|
const { roomId, controlledAudioDevices, callIntent } = getUrlParams();
|
||||||
if (roomId === null) throw Error("could not get roomId from url params");
|
if (roomId === null) throw Error("could not get roomId from url params");
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
if (room === null) throw Error("could not get room from client");
|
if (room === null) throw Error("could not get room from client");
|
||||||
@@ -128,7 +128,10 @@ export async function createMatrixRTCSdk(
|
|||||||
const rtcSession = rtcSessionManager.getRoomSession(room);
|
const rtcSession = rtcSessionManager.getRoomSession(room);
|
||||||
|
|
||||||
// media devices
|
// media devices
|
||||||
const mediaDevices = new MediaDevices(scope);
|
const mediaDevices = new MediaDevices(scope, {
|
||||||
|
controlledAudioDevices,
|
||||||
|
callIntent,
|
||||||
|
});
|
||||||
const muteStates = new MuteStates(scope, mediaDevices, {
|
const muteStates = new MuteStates(scope, mediaDevices, {
|
||||||
audioEnabled: false,
|
audioEnabled: false,
|
||||||
videoEnabled: false,
|
videoEnabled: false,
|
||||||
@@ -141,7 +144,11 @@ export async function createMatrixRTCSdk(
|
|||||||
room,
|
room,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
muteStates,
|
muteStates,
|
||||||
{ encryptionSystem: { kind: E2eeType.PER_PARTICIPANT } },
|
{
|
||||||
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
|
controlledAudioDevices,
|
||||||
|
callIntent,
|
||||||
|
},
|
||||||
of({}),
|
of({}),
|
||||||
of({}),
|
of({}),
|
||||||
constant({ supported: false, processor: undefined }),
|
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.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { type FC, type JSX, Suspense, useEffect, useState } from "react";
|
||||||
type FC,
|
|
||||||
type JSX,
|
|
||||||
Suspense,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { BrowserRouter, Route, useLocation, Routes } from "react-router-dom";
|
import { BrowserRouter, Route, useLocation, Routes } from "react-router-dom";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
@@ -32,7 +25,6 @@ import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
|||||||
import { type AppViewModel } from "./state/AppViewModel";
|
import { type AppViewModel } from "./state/AppViewModel";
|
||||||
import { MediaDevicesContext } from "./MediaDevicesContext";
|
import { MediaDevicesContext } from "./MediaDevicesContext";
|
||||||
import {
|
import {
|
||||||
getUrlParams,
|
|
||||||
HeaderStyle,
|
HeaderStyle,
|
||||||
UrlParamsProvider,
|
UrlParamsProvider,
|
||||||
useUrlParams,
|
useUrlParams,
|
||||||
@@ -75,6 +67,12 @@ const ThemeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
|||||||
return 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 {
|
interface Props {
|
||||||
vm: AppViewModel;
|
vm: AppViewModel;
|
||||||
}
|
}
|
||||||
@@ -91,9 +89,6 @@ export const App: FC<Props> = ({ vm }) => {
|
|||||||
.catch(logger.error);
|
.catch(logger.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Since we are outside the router component, we cannot use useUrlParams here
|
|
||||||
const { header } = useMemo(getUrlParams, []);
|
|
||||||
|
|
||||||
const content = loaded ? (
|
const content = loaded ? (
|
||||||
<ClientProvider>
|
<ClientProvider>
|
||||||
<MediaDevicesContext value={vm.mediaDevices}>
|
<MediaDevicesContext value={vm.mediaDevices}>
|
||||||
@@ -123,11 +118,7 @@ export const App: FC<Props> = ({ vm }) => {
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
{header === HeaderStyle.AppBar ? (
|
<MaybeAppBar>{content}</MaybeAppBar>
|
||||||
<AppBar>{content}</AppBar>
|
|
||||||
) : (
|
|
||||||
content
|
|
||||||
)}
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ const reactionData = {
|
|||||||
reactions$: new BehaviorSubject({}),
|
reactions$: new BehaviorSubject({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mediaDevices = new MediaDevices(globalScope);
|
const mediaDevices = new MediaDevices(globalScope, {
|
||||||
|
controlledAudioDevices: false,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper component that is used for:
|
* 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 { SpotlightTileViewModel } from "../state/TileViewModel";
|
||||||
import type { DeviceLabel } from "../state/MediaDevices";
|
import type { DeviceLabel } from "../state/MediaDevices";
|
||||||
import { createCallFooterViewModel } from "./CallFooterViewModel";
|
import { createCallFooterViewModel } from "./CallFooterViewModel";
|
||||||
|
import { HeaderStyle } from "../UrlParams";
|
||||||
|
|
||||||
const platformMock = vi.hoisted(() => vi.fn(() => "desktop"));
|
const platformMock = vi.hoisted(() => vi.fn(() => "desktop"));
|
||||||
vi.mock("../Platform", () => ({
|
vi.mock("../Platform", () => ({
|
||||||
@@ -105,6 +106,7 @@ describe("createCallFooterViewModel", () => {
|
|||||||
mockMuteStates(),
|
mockMuteStates(),
|
||||||
twoMicsAndOneCamMediaDevices,
|
twoMicsAndOneCamMediaDevices,
|
||||||
/* reactionIdentifier */ undefined,
|
/* reactionIdentifier */ undefined,
|
||||||
|
{ showControls: true, header: HeaderStyle.Standard },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(vm.audioOptions$.value).toEqual([]);
|
expect(vm.audioOptions$.value).toEqual([]);
|
||||||
@@ -126,6 +128,7 @@ describe("createCallFooterViewModel", () => {
|
|||||||
mockMuteStates(),
|
mockMuteStates(),
|
||||||
twoMicsAndOneCamMediaDevices,
|
twoMicsAndOneCamMediaDevices,
|
||||||
/* reactionIdentifier */ undefined,
|
/* reactionIdentifier */ undefined,
|
||||||
|
{ showControls: true, header: HeaderStyle.Standard },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(vm.audioOptions$?.value).toEqual([
|
expect(vm.audioOptions$?.value).toEqual([
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { type Behavior, constant } from "../state/Behavior";
|
|||||||
import type { ObservableScope } from "../state/ObservableScope";
|
import type { ObservableScope } from "../state/ObservableScope";
|
||||||
import { type MuteStates } from "../state/MuteStates";
|
import { type MuteStates } from "../state/MuteStates";
|
||||||
import { createStaticViewModel, type ViewModel } from "../state/ViewModel";
|
import { createStaticViewModel, type ViewModel } from "../state/ViewModel";
|
||||||
import { getUrlParams, HeaderStyle } from "../UrlParams";
|
import { HeaderStyle } from "../UrlParams";
|
||||||
import { platform } from "../Platform";
|
import { platform } from "../Platform";
|
||||||
import { type FooterSnapshot } from "./CallFooter";
|
import { type FooterSnapshot } from "./CallFooter";
|
||||||
|
|
||||||
@@ -138,6 +138,8 @@ function buildDeviceBehaviors(
|
|||||||
* @param mediaDevices - Available and selected input devices.
|
* @param mediaDevices - Available and selected input devices.
|
||||||
* @param reactionIdentifier - The local user's reaction identifier string, or
|
* @param reactionIdentifier - The local user's reaction identifier string, or
|
||||||
* undefined when reactions are not supported (hides the reaction button).
|
* 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(
|
export function createCallFooterViewModel(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
@@ -145,8 +147,9 @@ export function createCallFooterViewModel(
|
|||||||
muteStates: MuteStates,
|
muteStates: MuteStates,
|
||||||
mediaDevices: MediaDevices,
|
mediaDevices: MediaDevices,
|
||||||
reactionIdentifier: string | undefined,
|
reactionIdentifier: string | undefined,
|
||||||
|
options: { showControls: boolean; header: HeaderStyle },
|
||||||
): ViewModel<FooterSnapshot> {
|
): ViewModel<FooterSnapshot> {
|
||||||
const { showControls, header: headerStyle } = getUrlParams();
|
const { showControls, header: headerStyle } = options;
|
||||||
const showLogo = headerStyle === HeaderStyle.Standard;
|
const showLogo = headerStyle === HeaderStyle.Standard;
|
||||||
|
|
||||||
const isPip$ = scope.behavior(
|
const isPip$ = scope.behavior(
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import { MediaDevicesContext } from "../MediaDevicesContext";
|
|||||||
import { MediaDevices } from "../state/MediaDevices";
|
import { MediaDevices } from "../state/MediaDevices";
|
||||||
import { globalScope } from "../state/ObservableScope";
|
import { globalScope } from "../state/ObservableScope";
|
||||||
|
|
||||||
const mediaDevices = new MediaDevices(globalScope);
|
const mediaDevices = new MediaDevices(globalScope, {
|
||||||
|
controlledAudioDevices: false,
|
||||||
|
});
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
component: MediaMuteAndSwitchButton,
|
component: MediaMuteAndSwitchButton,
|
||||||
|
|||||||
+10
-1
@@ -21,6 +21,7 @@ import { init as initRageshake } from "./settings/rageshake";
|
|||||||
import { Initializer } from "./initializer";
|
import { Initializer } from "./initializer";
|
||||||
import { AppViewModel } from "./state/AppViewModel";
|
import { AppViewModel } from "./state/AppViewModel";
|
||||||
import { globalScope } from "./state/ObservableScope";
|
import { globalScope } from "./state/ObservableScope";
|
||||||
|
import { getUrlParams } from "./UrlParams";
|
||||||
|
|
||||||
initRageshake().catch((e) => {
|
initRageshake().catch((e) => {
|
||||||
logger.error("Failed to initialize rageshake", e);
|
logger.error("Failed to initialize rageshake", e);
|
||||||
@@ -49,9 +50,17 @@ if (fatalError !== null) {
|
|||||||
|
|
||||||
Initializer.initBeforeReact()
|
Initializer.initBeforeReact()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
const { controlledAudioDevices, callIntent } = getUrlParams();
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App vm={new AppViewModel(globalScope)} />
|
<App
|
||||||
|
vm={
|
||||||
|
new AppViewModel(globalScope, {
|
||||||
|
controlledAudioDevices,
|
||||||
|
callIntent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,12 +54,7 @@ import { useRoomAvatar } from "./useRoomAvatar";
|
|||||||
import { useRoomName } from "./useRoomName";
|
import { useRoomName } from "./useRoomName";
|
||||||
import { useJoinRule } from "./useJoinRule";
|
import { useJoinRule } from "./useJoinRule";
|
||||||
import { InviteModal } from "./InviteModal";
|
import { InviteModal } from "./InviteModal";
|
||||||
import {
|
import { HeaderStyle, type UrlParams, useUrlParams } from "../UrlParams";
|
||||||
getUrlParams,
|
|
||||||
HeaderStyle,
|
|
||||||
type UrlParams,
|
|
||||||
useUrlParams,
|
|
||||||
} from "../UrlParams";
|
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { useAudioContext } from "../useAudioContext";
|
import { useAudioContext } from "../useAudioContext";
|
||||||
import {
|
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
|
// 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.
|
// error occurs we should keep the widget open until the user reads it.
|
||||||
if (reason != "error" && !getUrlParams().returnToLobby) {
|
if (reason != "error" && !returnToLobby) {
|
||||||
try {
|
try {
|
||||||
await widget.api.transport.send(ElementWidgetActions.Close, {});
|
await widget.api.transport.send(ElementWidgetActions.Close, {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -425,6 +420,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
rtcSession,
|
rtcSession,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
confineToRoom,
|
confineToRoom,
|
||||||
|
returnToLobby,
|
||||||
navigate,
|
navigate,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
+17
-2
@@ -122,8 +122,16 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
rootLogger.info("START CALL VIEW SCOPE");
|
rootLogger.info("START CALL VIEW SCOPE");
|
||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
const reactionsReader = new ReactionsReader(scope, props.rtcSession);
|
const reactionsReader = new ReactionsReader(scope, props.rtcSession);
|
||||||
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
|
const {
|
||||||
urlParams;
|
autoLeaveWhenOthersLeft,
|
||||||
|
waitForCallPickup,
|
||||||
|
sendNotificationType,
|
||||||
|
controlledAudioDevices,
|
||||||
|
header,
|
||||||
|
showControls,
|
||||||
|
hideScreensharing,
|
||||||
|
callIntent,
|
||||||
|
} = urlParams;
|
||||||
|
|
||||||
const vm = createCallViewModel$(
|
const vm = createCallViewModel$(
|
||||||
scope,
|
scope,
|
||||||
@@ -136,6 +144,12 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
autoLeaveWhenOthersLeft,
|
autoLeaveWhenOthersLeft,
|
||||||
waitForCallPickup: waitForCallPickup && sendNotificationType === "ring",
|
waitForCallPickup: waitForCallPickup && sendNotificationType === "ring",
|
||||||
matrixRTCMode$: matrixRTCModeSetting.value$,
|
matrixRTCMode$: matrixRTCModeSetting.value$,
|
||||||
|
controlledAudioDevices,
|
||||||
|
header,
|
||||||
|
showControls,
|
||||||
|
hideScreensharing,
|
||||||
|
sendNotificationType,
|
||||||
|
callIntent,
|
||||||
},
|
},
|
||||||
reactionsReader.raisedHands$,
|
reactionsReader.raisedHands$,
|
||||||
reactionsReader.reactions$,
|
reactionsReader.reactions$,
|
||||||
@@ -172,6 +186,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
props.muteStates,
|
props.muteStates,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
`${props.client.getUserId()}:${props.client.getDeviceId()}`,
|
`${props.client.getUserId()}:${props.client.getDeviceId()}`,
|
||||||
|
{ showControls: urlParams.showControls, header: urlParams.header },
|
||||||
);
|
);
|
||||||
setFooterVm(footerVm);
|
setFooterVm(footerVm);
|
||||||
setDeveloperSettingsVm(createDeveloperSettingsTabViewModel(scope, vm));
|
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.
|
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";
|
import { type ObservableScope } from "./ObservableScope";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The top-level state holder for the application.
|
* The top-level state holder for the application.
|
||||||
*/
|
*/
|
||||||
export class AppViewModel {
|
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
|
// TODO: Move more application logic here. The CallViewModel, at the very
|
||||||
// least, ought to be accessible from this object.
|
// 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", () => {
|
it.skip("audio output changes when toggling earpiece mode", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
getUrlParams.mockReturnValue({ controlledAudioDevices: true });
|
|
||||||
vi.mocked(ComponentsCore.createMediaDeviceObserver).mockReturnValue(
|
vi.mocked(ComponentsCore.createMediaDeviceObserver).mockReturnValue(
|
||||||
of([]),
|
of([]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const devices = new MediaDevices(testScope());
|
const devices = new MediaDevices(testScope(), {
|
||||||
|
controlledAudioDevices: true,
|
||||||
|
});
|
||||||
|
|
||||||
window.controls.setAvailableAudioDevices([
|
window.controls.setAvailableAudioDevices([
|
||||||
{ id: "speaker", name: "Speaker", isSpeaker: true },
|
{ id: "speaker", name: "Speaker", isSpeaker: true },
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ import {
|
|||||||
MembershipManagerEvent,
|
MembershipManagerEvent,
|
||||||
type LivekitTransportConfig,
|
type LivekitTransportConfig,
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
|
type RTCCallIntent,
|
||||||
|
type RTCNotificationType,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { type IWidgetApiRequest } from "matrix-widget-api";
|
import { type IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
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 { E2eeType } from "../../e2ee/e2eeType";
|
||||||
import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider";
|
import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider";
|
||||||
import { type MuteStates } from "../MuteStates";
|
import { type MuteStates } from "../MuteStates";
|
||||||
import { getUrlParams, HeaderStyle } from "../../UrlParams";
|
import { HeaderStyle } from "../../UrlParams";
|
||||||
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
||||||
import { ElementWidgetActions, widget } from "../../widget";
|
import { ElementWidgetActions, widget } from "../../widget";
|
||||||
import {
|
import {
|
||||||
@@ -171,6 +173,23 @@ import { type GridTileViewModel } from "../TileViewModel.ts";
|
|||||||
// callMembership -> rtcMembership
|
// callMembership -> rtcMembership
|
||||||
export interface CallViewModelOptions {
|
export interface CallViewModelOptions {
|
||||||
encryptionSystem: EncryptionSystem;
|
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;
|
autoLeaveWhenOthersLeft?: boolean;
|
||||||
/**
|
/**
|
||||||
* If the call is started in a way where we want it to behave like a telephone usecase
|
* 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))
|
if (!(userId && deviceId))
|
||||||
throw new UnknownCallError(new Error("userId and deviceId are required"));
|
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(
|
const livekitKeyProvider = getE2eeKeyProvider(
|
||||||
options.encryptionSystem,
|
options.encryptionSystem,
|
||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
@@ -523,7 +553,7 @@ export function createCallViewModel$(
|
|||||||
mediaDevices,
|
mediaDevices,
|
||||||
trackProcessorState$,
|
trackProcessorState$,
|
||||||
livekitKeyProvider,
|
livekitKeyProvider,
|
||||||
getUrlParams().controlledAudioDevices,
|
controlledAudioDevices,
|
||||||
options.livekitRoomFactory,
|
options.livekitRoomFactory,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -563,6 +593,8 @@ export function createCallViewModel$(
|
|||||||
encryptMedia: livekitKeyProvider !== undefined,
|
encryptMedia: livekitKeyProvider !== undefined,
|
||||||
// TODO. This might need to get called again on each change of matrixRTCMode...
|
// TODO. This might need to get called again on each change of matrixRTCMode...
|
||||||
matrixRTCMode: mode,
|
matrixRTCMode: mode,
|
||||||
|
sendNotificationType,
|
||||||
|
callIntent,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -592,12 +624,14 @@ export function createCallViewModel$(
|
|||||||
logger.getChild(
|
logger.getChild(
|
||||||
"[Publisher " + connection.transport.livekit_service_url + "]",
|
"[Publisher " + connection.transport.livekit_service_url + "]",
|
||||||
),
|
),
|
||||||
|
controlledAudioDevices,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
connectionManager,
|
connectionManager,
|
||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
localTransport$,
|
localTransport$,
|
||||||
roomId: matrixRoom.roomId,
|
roomId: matrixRoom.roomId,
|
||||||
|
hideScreensharing,
|
||||||
logger: logger.getChild(`[${Date.now()}]`),
|
logger: logger.getChild(`[${Date.now()}]`),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1457,9 +1491,8 @@ export function createCallViewModel$(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const urlParams = getUrlParams();
|
|
||||||
const showFooterUrlParams = !(
|
const showFooterUrlParams = !(
|
||||||
urlParams.header === HeaderStyle.None && urlParams.showControls === false
|
header === HeaderStyle.None && showControls === false
|
||||||
);
|
);
|
||||||
const showFooter$ = scope.behavior(
|
const showFooter$ = scope.behavior(
|
||||||
naturallyShowFooter$.pipe(
|
naturallyShowFooter$.pipe(
|
||||||
@@ -1778,8 +1811,7 @@ export function createCallViewModel$(
|
|||||||
return {
|
return {
|
||||||
autoLeave$: autoLeave$,
|
autoLeave$: autoLeave$,
|
||||||
ringingVm$: ringingMedia$,
|
ringingVm$: ringingMedia$,
|
||||||
ringingStatusLocation:
|
ringingStatusLocation: header === HeaderStyle.AppBar ? "app_bar" : "tile",
|
||||||
urlParams.header === HeaderStyle.AppBar ? "app_bar" : "tile",
|
|
||||||
leave$: leave$,
|
leave$: leave$,
|
||||||
hangup: (): void => userHangup$.next(),
|
hangup: (): void => userHangup$.next(),
|
||||||
join: localMembership.requestJoinAndPublish,
|
join: localMembership.requestJoinAndPublish,
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ describe("LocalMembership", () => {
|
|||||||
rtsSession$: constant(RTCMemberStatus.Connected),
|
rtsSession$: constant(RTCMemberStatus.Connected),
|
||||||
},
|
},
|
||||||
roomId: "!test-room-id:example.org",
|
roomId: "!test-room-id:example.org",
|
||||||
|
hideScreensharing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("throws error on missing RTC config error", () => {
|
it("throws error on missing RTC config error", () => {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
type LivekitTransport,
|
type LivekitTransport,
|
||||||
type LivekitTransportConfig,
|
type LivekitTransportConfig,
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
|
type RTCCallIntent,
|
||||||
|
type RTCNotificationType,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
@@ -52,7 +54,7 @@ import {
|
|||||||
UnknownCallError,
|
UnknownCallError,
|
||||||
} from "../../../utils/errors.ts";
|
} from "../../../utils/errors.ts";
|
||||||
import { ElementWidgetActions, widget } from "../../../widget.ts";
|
import { ElementWidgetActions, widget } from "../../../widget.ts";
|
||||||
import { getUrlParams } from "../../../UrlParams.ts";
|
|
||||||
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts";
|
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts";
|
||||||
import {
|
import {
|
||||||
advancedScreenShare,
|
advancedScreenShare,
|
||||||
@@ -141,6 +143,8 @@ interface Props {
|
|||||||
MatrixRTCSession,
|
MatrixRTCSession,
|
||||||
"updateCallIntent" | "leaveRoomSession"
|
"updateCallIntent" | "leaveRoomSession"
|
||||||
>;
|
>;
|
||||||
|
/** Whether to hide the screen-sharing button. */
|
||||||
|
hideScreensharing: boolean;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +164,7 @@ interface Props {
|
|||||||
* @param props.muteStates The mute states for video and audio.
|
* @param props.muteStates The mute states for video and audio.
|
||||||
* @param props.matrixRTCSession The matrix RTC session to join.
|
* @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.roomId The room ID used as the call identifier in analytics events.
|
||||||
|
* @param props.hideScreensharing Whether to hide the screen-sharing button.
|
||||||
* @returns
|
* @returns
|
||||||
* - publisher: The handle to create tracks and publish them to the room.
|
* - 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)
|
* - 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,
|
muteStates,
|
||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
roomId,
|
roomId,
|
||||||
|
hideScreensharing,
|
||||||
}: Props): {
|
}: Props): {
|
||||||
/**
|
/**
|
||||||
* This request to start audio and video tracks.
|
* This request to start audio and video tracks.
|
||||||
@@ -709,7 +715,7 @@ export const createLocalMembership$ = ({
|
|||||||
let toggleScreenSharing: (() => void) | null = null;
|
let toggleScreenSharing: (() => void) | null = null;
|
||||||
if (
|
if (
|
||||||
"getDisplayMedia" in (navigator.mediaDevices ?? {}) &&
|
"getDisplayMedia" in (navigator.mediaDevices ?? {}) &&
|
||||||
!getUrlParams().hideScreensharing
|
!hideScreensharing
|
||||||
) {
|
) {
|
||||||
toggleScreenSharing = (): void => {
|
toggleScreenSharing = (): void => {
|
||||||
const screenshareSettings: ScreenShareCaptureOptions = {
|
const screenshareSettings: ScreenShareCaptureOptions = {
|
||||||
@@ -820,6 +826,10 @@ export function observeSharingScreen$(p: Participant): Observable<boolean> {
|
|||||||
interface EnterRTCSessionOptions {
|
interface EnterRTCSessionOptions {
|
||||||
encryptMedia: boolean;
|
encryptMedia: boolean;
|
||||||
matrixRTCMode: MatrixRTCMode;
|
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 rtcSession - The MatrixRTCSession to join.
|
||||||
* @param ownMembershipIdentity - Options for entering the RTC session.
|
* @param ownMembershipIdentity - Options for entering the RTC session.
|
||||||
* @param transport - The LivekitTransport to use for this 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.
|
* @throws If the widget could not send ElementWidgetActions.JoinCall action.
|
||||||
*/
|
*/
|
||||||
// Exported for unit testing
|
// Exported for unit testing
|
||||||
@@ -842,7 +854,12 @@ export function enterRTCSession(
|
|||||||
transport: LivekitTransportConfig,
|
transport: LivekitTransportConfig,
|
||||||
options: EnterRTCSessionOptions,
|
options: EnterRTCSessionOptions,
|
||||||
): void {
|
): void {
|
||||||
const { encryptMedia, matrixRTCMode } = options;
|
const {
|
||||||
|
encryptMedia,
|
||||||
|
matrixRTCMode,
|
||||||
|
sendNotificationType: notificationType,
|
||||||
|
callIntent,
|
||||||
|
} = options;
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||||
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
||||||
|
|
||||||
@@ -851,7 +868,6 @@ export function enterRTCSession(
|
|||||||
// groupCallOTelMembership?.onJoinCall();
|
// groupCallOTelMembership?.onJoinCall();
|
||||||
|
|
||||||
const { matrix_rtc_session: matrixRtcSessionConfig } = Config.get();
|
const { matrix_rtc_session: matrixRtcSessionConfig } = Config.get();
|
||||||
const { sendNotificationType: notificationType, callIntent } = getUrlParams();
|
|
||||||
const multiSFU =
|
const multiSFU =
|
||||||
matrixRTCMode === MatrixRTCMode.Compatibility ||
|
matrixRTCMode === MatrixRTCMode.Compatibility ||
|
||||||
matrixRTCMode === MatrixRTCMode.Matrix_2_0;
|
matrixRTCMode === MatrixRTCMode.Matrix_2_0;
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ describe("Publisher", () => {
|
|||||||
muteStates,
|
muteStates,
|
||||||
constant({ supported: false, processor: undefined }),
|
constant({ supported: false, processor: undefined }),
|
||||||
logger,
|
logger,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -309,6 +310,7 @@ describe("Publisher", () => {
|
|||||||
muteStates,
|
muteStates,
|
||||||
constant({ supported: false, processor: undefined }),
|
constant({ supported: false, processor: undefined }),
|
||||||
logger,
|
logger,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -364,6 +366,7 @@ describe("Bug fix", () => {
|
|||||||
muteStates,
|
muteStates,
|
||||||
constant({ supported: false, processor: undefined }),
|
constant({ supported: false, processor: undefined }),
|
||||||
logger,
|
logger,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
audioEnabled$.next(true);
|
audioEnabled$.next(true);
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
type ProcessorState,
|
type ProcessorState,
|
||||||
trackProcessorSync,
|
trackProcessorSync,
|
||||||
} from "../../../livekit/TrackProcessorContext.tsx";
|
} from "../../../livekit/TrackProcessorContext.tsx";
|
||||||
import { getUrlParams } from "../../../UrlParams.ts";
|
|
||||||
import { observeTrackReference$ } from "../../observeTrackReference";
|
import { observeTrackReference$ } from "../../observeTrackReference";
|
||||||
import { type Connection } from "../remoteMembers/Connection.ts";
|
import { type Connection } from "../remoteMembers/Connection.ts";
|
||||||
import { ObservableScope } from "../../ObservableScope.ts";
|
import { ObservableScope } from "../../ObservableScope.ts";
|
||||||
@@ -56,6 +56,8 @@ export class Publisher {
|
|||||||
* @param muteStates - The mute states for audio and video.
|
* @param muteStates - The mute states for audio and video.
|
||||||
* @param trackerProcessorState$ - The processor state for the video track processor (e.g. background blur).
|
* @param trackerProcessorState$ - The processor state for the video track processor (e.g. background blur).
|
||||||
* @param logger - The logger to use for logging :D.
|
* @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(
|
public constructor(
|
||||||
private connection: Pick<Connection, "livekitRoom" | "state$">, //setE2EEEnabled,
|
private connection: Pick<Connection, "livekitRoom" | "state$">, //setE2EEEnabled,
|
||||||
@@ -63,8 +65,8 @@ export class Publisher {
|
|||||||
private readonly muteStates: MuteStates,
|
private readonly muteStates: MuteStates,
|
||||||
trackerProcessorState$: Behavior<ProcessorState>,
|
trackerProcessorState$: Behavior<ProcessorState>,
|
||||||
private logger: Logger,
|
private logger: Logger,
|
||||||
|
controlledAudioDevices: boolean,
|
||||||
) {
|
) {
|
||||||
const { controlledAudioDevices } = getUrlParams();
|
|
||||||
const room = connection.livekitRoom;
|
const room = connection.livekitRoom;
|
||||||
|
|
||||||
room.setE2EEEnabled(room.options.e2ee !== undefined)?.catch((e: Error) => {
|
room.setE2EEEnabled(room.options.e2ee !== undefined)?.catch((e: Error) => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { createMediaDeviceObserver } from "@livekit/components-core";
|
import { createMediaDeviceObserver } from "@livekit/components-core";
|
||||||
import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
|
import { type RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
|
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
|
||||||
@@ -25,7 +26,6 @@ import {
|
|||||||
} from "../settings/settings";
|
} from "../settings/settings";
|
||||||
import { type ObservableScope } from "./ObservableScope";
|
import { type ObservableScope } from "./ObservableScope";
|
||||||
import { availableOutputDevices$ as controlledAvailableOutputDevices$ } from "../controls";
|
import { availableOutputDevices$ as controlledAvailableOutputDevices$ } from "../controls";
|
||||||
import { getUrlParams } from "../UrlParams";
|
|
||||||
import { platform } from "../Platform";
|
import { platform } from "../Platform";
|
||||||
import { switchWhen } from "../utils/observable";
|
import { switchWhen } from "../utils/observable";
|
||||||
import { type Behavior, constant } from "./Behavior";
|
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 {
|
export class MediaDevices {
|
||||||
private readonly deviceNamesRequest$ = new Subject<void>();
|
private readonly deviceNamesRequest$ = new Subject<void>();
|
||||||
/**
|
/**
|
||||||
@@ -368,23 +384,28 @@ export class MediaDevices {
|
|||||||
public readonly audioOutput: MediaDevice<
|
public readonly audioOutput: MediaDevice<
|
||||||
AudioOutputDeviceLabel,
|
AudioOutputDeviceLabel,
|
||||||
SelectedAudioOutputDevice
|
SelectedAudioOutputDevice
|
||||||
> = getUrlParams().controlledAudioDevices
|
> = this.audioOutputOptions.controlledAudioDevices
|
||||||
? platform == "android"
|
? platform == "android"
|
||||||
? new AndroidControlledAudioOutput(
|
? new AndroidControlledAudioOutput(
|
||||||
controlledAvailableOutputDevices$,
|
controlledAvailableOutputDevices$,
|
||||||
this.scope,
|
this.scope,
|
||||||
getUrlParams().callIntent,
|
this.audioOutputOptions.callIntent,
|
||||||
window.controls,
|
window.controls,
|
||||||
)
|
)
|
||||||
: new IOSControlledAudioOutput(
|
: new IOSControlledAudioOutput(
|
||||||
this.usingNames$,
|
this.usingNames$,
|
||||||
this.scope,
|
this.scope,
|
||||||
getUrlParams().callIntent,
|
this.audioOutputOptions.callIntent,
|
||||||
)
|
)
|
||||||
: new AudioOutput(this.usingNames$, this.scope);
|
: new AudioOutput(this.usingNames$, this.scope);
|
||||||
|
|
||||||
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
||||||
new VideoInput(this.usingNames$, this.scope);
|
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 { WidgetApiToWidgetAction } from "matrix-widget-api";
|
||||||
|
|
||||||
import { useTheme } from "./useTheme";
|
import { useTheme } from "./useTheme";
|
||||||
import { getUrlParams } from "./UrlParams";
|
import { useUrlParams } from "./UrlParams";
|
||||||
import { widget } from "./widget";
|
import { widget } from "./widget";
|
||||||
|
|
||||||
vi.mock("./UrlParams", () => ({ getUrlParams: vi.fn() }));
|
vi.mock("./UrlParams", () => ({ useUrlParams: vi.fn() }));
|
||||||
vi.mock("./widget", () => ({
|
vi.mock("./widget", () => ({
|
||||||
widget: {
|
widget: {
|
||||||
api: { transport: { reply: vi.fn() } },
|
api: { transport: { reply: vi.fn() } },
|
||||||
@@ -39,7 +39,7 @@ describe("useTheme", () => {
|
|||||||
vi.spyOn(originalClassList, "add");
|
vi.spyOn(originalClassList, "add");
|
||||||
vi.spyOn(originalClassList, "remove");
|
vi.spyOn(originalClassList, "remove");
|
||||||
vi.spyOn(originalClassList, "item").mockReturnValue(null);
|
vi.spyOn(originalClassList, "item").mockReturnValue(null);
|
||||||
(getUrlParams as Mock).mockReturnValue({ theme: "dark" });
|
(useUrlParams as Mock).mockReturnValue({ theme: "dark" });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -53,7 +53,7 @@ describe("useTheme", () => {
|
|||||||
{ setTheme: "light-high-contrast", add: ["cpd-theme-light-hc"] },
|
{ setTheme: "light-high-contrast", add: ["cpd-theme-light-hc"] },
|
||||||
])("apply procedure", ({ setTheme, add }) => {
|
])("apply procedure", ({ setTheme, add }) => {
|
||||||
test(`should apply ${add[0]} theme when ${setTheme} theme is specified`, () => {
|
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());
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -9,15 +9,14 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|||||||
import { WidgetApiToWidgetAction } from "matrix-widget-api";
|
import { WidgetApiToWidgetAction } from "matrix-widget-api";
|
||||||
import { type IThemeChangeActionRequest } from "matrix-widget-api";
|
import { type IThemeChangeActionRequest } from "matrix-widget-api";
|
||||||
|
|
||||||
import { getUrlParams } from "./UrlParams";
|
import { useUrlParams } from "./UrlParams";
|
||||||
import { widget } from "./widget";
|
import { widget } from "./widget";
|
||||||
import { useRootElement } from "./RootElementContext";
|
import { useRootElement } from "./RootElementContext";
|
||||||
|
|
||||||
export const useTheme = (): void => {
|
export const useTheme = (): void => {
|
||||||
const rootElement = useRootElement();
|
const rootElement = useRootElement();
|
||||||
const [requestedTheme, setRequestedTheme] = useState(
|
const { theme } = useUrlParams();
|
||||||
() => getUrlParams().theme,
|
const [requestedTheme, setRequestedTheme] = useState(theme);
|
||||||
);
|
|
||||||
const previousTheme = useRef<string | null>(rootElement.classList.item(0));
|
const previousTheme = useRef<string | null>(rootElement.classList.item(0));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
|||||||
import { constant } from "../state/Behavior";
|
import { constant } from "../state/Behavior";
|
||||||
import { MatrixRTCMode } from "../config/ConfigOptions";
|
import { MatrixRTCMode } from "../config/ConfigOptions";
|
||||||
import { createCallFooterViewModel } from "../components/CallFooterViewModel";
|
import { createCallFooterViewModel } from "../components/CallFooterViewModel";
|
||||||
|
import { HeaderStyle } from "../UrlParams";
|
||||||
import { type FooterSnapshot } from "../components/CallFooter";
|
import { type FooterSnapshot } from "../components/CallFooter";
|
||||||
import { type ViewModel } from "../state/ViewModel";
|
import { type ViewModel } from "../state/ViewModel";
|
||||||
import { createDeveloperSettingsTabViewModel } from "../settings/DeveloperSettingsTabViewModel";
|
import { createDeveloperSettingsTabViewModel } from "../settings/DeveloperSettingsTabViewModel";
|
||||||
@@ -187,6 +188,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
muteStates,
|
muteStates,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
"reactionId",
|
"reactionId",
|
||||||
|
{ showControls: true, header: HeaderStyle.Standard },
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
vm,
|
vm,
|
||||||
|
|||||||
Reference in New Issue
Block a user