mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
Share the mapping from parameters to call view model options
The SDK stopped sending join notifications: it threaded callIntent into createCallViewModel$ but not its pair sendNotificationType, which enterRTCSession used to read for itself, so an explicit ?sendNotificationType=ring — or an intent that implies one — no longer reached joinRTCSession. The mechanism is worth fixing rather than the instance. The defaults on CallViewModelOptions describe a standalone Element Call, so a widget caller that misses a field gets standalone behaviour rather than an error, and the SDK is only ever a widget. Give both callers one shared mapping so they cannot drift, and cover the whole chain from URL to options in tests. autoLeaveWhenOthersLeft and waitForCallPickup stay out of it: the view model never read those from the parameters, so enabling them for the SDK would be a change in its behaviour rather than a fix.
This commit is contained in:
+7
-4
@@ -46,7 +46,10 @@ import {
|
|||||||
// Can this be done in the tsconfig.json
|
// Can this be done in the tsconfig.json
|
||||||
import { type TextStreamInfo } from "../node_modules/livekit-client/dist/src/room/types";
|
import { type TextStreamInfo } from "../node_modules/livekit-client/dist/src/room/types";
|
||||||
import { type Behavior, constant } from "../src/state/Behavior";
|
import { type Behavior, constant } from "../src/state/Behavior";
|
||||||
import { createCallViewModel$ } from "../src/state/CallViewModel/CallViewModel";
|
import {
|
||||||
|
callViewModelOptionsFromParams,
|
||||||
|
createCallViewModel$,
|
||||||
|
} from "../src/state/CallViewModel/CallViewModel";
|
||||||
import { ObservableScope } from "../src/state/ObservableScope";
|
import { ObservableScope } from "../src/state/ObservableScope";
|
||||||
import { getUrlParams } from "../src/UrlParams";
|
import { getUrlParams } from "../src/UrlParams";
|
||||||
import { MuteStates } from "../src/state/MuteStates";
|
import { MuteStates } from "../src/state/MuteStates";
|
||||||
@@ -113,7 +116,8 @@ export async function createMatrixRTCSdk(
|
|||||||
logger.info("client created");
|
logger.info("client created");
|
||||||
|
|
||||||
// url params
|
// url params
|
||||||
const { roomId, controlledAudioDevices, callIntent } = getUrlParams();
|
const urlParams = getUrlParams();
|
||||||
|
const { roomId, controlledAudioDevices, callIntent } = urlParams;
|
||||||
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");
|
||||||
@@ -144,10 +148,9 @@ export async function createMatrixRTCSdk(
|
|||||||
mediaDevices,
|
mediaDevices,
|
||||||
muteStates,
|
muteStates,
|
||||||
{
|
{
|
||||||
|
...callViewModelOptionsFromParams(urlParams),
|
||||||
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
hostBridge,
|
hostBridge,
|
||||||
controlledAudioDevices,
|
|
||||||
callIntent,
|
|
||||||
},
|
},
|
||||||
of({}),
|
of({}),
|
||||||
of({}),
|
of({}),
|
||||||
|
|||||||
+4
-16
@@ -41,6 +41,7 @@ import { type MatrixInfo } from "./VideoPreview";
|
|||||||
import { InviteButton } from "../button/InviteButton";
|
import { InviteButton } from "../button/InviteButton";
|
||||||
import {
|
import {
|
||||||
type CallViewModel,
|
type CallViewModel,
|
||||||
|
callViewModelOptionsFromParams,
|
||||||
createCallViewModel$,
|
createCallViewModel$,
|
||||||
} from "../state/CallViewModel/CallViewModel.ts";
|
} from "../state/CallViewModel/CallViewModel.ts";
|
||||||
import { Grid, type TileProps } from "../grid/Grid";
|
import { Grid, type TileProps } from "../grid/Grid";
|
||||||
@@ -123,16 +124,8 @@ 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 {
|
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
|
||||||
autoLeaveWhenOthersLeft,
|
urlParams;
|
||||||
waitForCallPickup,
|
|
||||||
sendNotificationType,
|
|
||||||
controlledAudioDevices,
|
|
||||||
header,
|
|
||||||
showControls,
|
|
||||||
hideScreensharing,
|
|
||||||
callIntent,
|
|
||||||
} = urlParams;
|
|
||||||
|
|
||||||
const vm = createCallViewModel$(
|
const vm = createCallViewModel$(
|
||||||
scope,
|
scope,
|
||||||
@@ -141,17 +134,12 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
mediaDevices,
|
mediaDevices,
|
||||||
props.muteStates,
|
props.muteStates,
|
||||||
{
|
{
|
||||||
|
...callViewModelOptionsFromParams(urlParams),
|
||||||
encryptionSystem: props.e2eeSystem,
|
encryptionSystem: props.e2eeSystem,
|
||||||
hostBridge,
|
hostBridge,
|
||||||
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$,
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ import {
|
|||||||
} from "./CallViewModelTestUtils.ts";
|
} from "./CallViewModelTestUtils.ts";
|
||||||
import { MatrixRTCMode } from "../../config/ConfigOptions.ts";
|
import { MatrixRTCMode } from "../../config/ConfigOptions.ts";
|
||||||
import { initializeWidget } from "../../widget.ts";
|
import { initializeWidget } from "../../widget.ts";
|
||||||
|
import { computeUrlParams } from "../../UrlParams.ts";
|
||||||
|
import { callViewModelOptionsFromParams } from "./CallViewModel.ts";
|
||||||
|
|
||||||
initializeWidget();
|
initializeWidget();
|
||||||
|
|
||||||
@@ -83,9 +85,6 @@ vi.mock("livekit-client/e2ee-worker?worker");
|
|||||||
|
|
||||||
vi.mock("../e2ee/matrixKeyProvider");
|
vi.mock("../e2ee/matrixKeyProvider");
|
||||||
|
|
||||||
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
|
||||||
vi.mock("../UrlParams", () => ({ getUrlParams }));
|
|
||||||
|
|
||||||
const getPlatform = vi.hoisted(() => vi.fn(() => "desktop"));
|
const getPlatform = vi.hoisted(() => vi.fn(() => "desktop"));
|
||||||
vi.mock("../../Platform", () => ({
|
vi.mock("../../Platform", () => ({
|
||||||
get platform(): string {
|
get platform(): string {
|
||||||
@@ -1701,3 +1700,42 @@ describe.each(modes)("CallViewModel (%s mode)", (mode) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("callViewModelOptionsFromParams", () => {
|
||||||
|
// The defaults on CallViewModelOptions describe a standalone Element Call, so
|
||||||
|
// a widget caller that drops one of these gets standalone behaviour rather
|
||||||
|
// than an error. These check the whole chain from URL to options, which is
|
||||||
|
// where that went wrong for the SDK.
|
||||||
|
const widgetUrl = (extra: string): string =>
|
||||||
|
`#?widgetId=id&parentUrl=${encodeURIComponent("http://parent")}&${extra}`;
|
||||||
|
|
||||||
|
it("carries an explicitly requested notification type", () => {
|
||||||
|
const params = computeUrlParams("", widgetUrl("sendNotificationType=ring"));
|
||||||
|
expect(callViewModelOptionsFromParams(params).sendNotificationType).toBe(
|
||||||
|
"ring",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("carries the notification type an intent implies", () => {
|
||||||
|
const params = computeUrlParams("", widgetUrl("intent=start_call_dm"));
|
||||||
|
expect(callViewModelOptionsFromParams(params).sendNotificationType).toBe(
|
||||||
|
"ring",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("carries hideScreensharing", () => {
|
||||||
|
const params = computeUrlParams("", widgetUrl("hideScreensharing=true"));
|
||||||
|
expect(callViewModelOptionsFromParams(params).hideScreensharing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("carries controlledAudioDevices and the call intent", () => {
|
||||||
|
const params = computeUrlParams(
|
||||||
|
"",
|
||||||
|
widgetUrl("controlledAudioDevices=true&intent=start_call_voice"),
|
||||||
|
);
|
||||||
|
expect(callViewModelOptionsFromParams(params)).toMatchObject({
|
||||||
|
controlledAudioDevices: true,
|
||||||
|
callIntent: "audio",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -86,7 +86,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 { HeaderStyle } from "../../UrlParams";
|
import { HeaderStyle, type UrlParams } from "../../UrlParams";
|
||||||
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
||||||
import { type HostBridge, nullHostBridge } from "../../HostBridge";
|
import { type HostBridge, nullHostBridge } from "../../HostBridge";
|
||||||
import {
|
import {
|
||||||
@@ -216,6 +216,40 @@ export interface CallViewModelOptions {
|
|||||||
toggleScreensharing?: () => void;
|
toggleScreensharing?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options {@link createCallViewModel$} takes from the parameters Element
|
||||||
|
* Call was started with.
|
||||||
|
*
|
||||||
|
* Callers share this rather than picking the fields out themselves. The
|
||||||
|
* defaults on {@link CallViewModelOptions} describe a standalone Element Call,
|
||||||
|
* so a widget or embedded caller that misses one does not get an error — it
|
||||||
|
* quietly gets standalone behaviour instead.
|
||||||
|
*
|
||||||
|
* Note `autoLeaveWhenOthersLeft` and `waitForCallPickup` are deliberately not
|
||||||
|
* here: unlike these, the view model never read them from the parameters
|
||||||
|
* itself, so they remain the caller's decision.
|
||||||
|
*/
|
||||||
|
export function callViewModelOptionsFromParams(
|
||||||
|
params: UrlParams,
|
||||||
|
): Pick<
|
||||||
|
CallViewModelOptions,
|
||||||
|
| "controlledAudioDevices"
|
||||||
|
| "header"
|
||||||
|
| "showControls"
|
||||||
|
| "hideScreensharing"
|
||||||
|
| "sendNotificationType"
|
||||||
|
| "callIntent"
|
||||||
|
> {
|
||||||
|
return {
|
||||||
|
controlledAudioDevices: params.controlledAudioDevices,
|
||||||
|
header: params.header,
|
||||||
|
showControls: params.showControls,
|
||||||
|
hideScreensharing: params.hideScreensharing,
|
||||||
|
sendNotificationType: params.sendNotificationType,
|
||||||
|
callIntent: params.callIntent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Do not play any sounds if the participant count has exceeded this
|
// Do not play any sounds if the participant count has exceeded this
|
||||||
// number.
|
// number.
|
||||||
export const MAX_PARTICIPANT_COUNT_FOR_SOUND = 8;
|
export const MAX_PARTICIPANT_COUNT_FOR_SOUND = 8;
|
||||||
|
|||||||
Reference in New Issue
Block a user