mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
Talk to a component host in callbacks, not observables
The component exposed the internal HostBridge to hosts as-is, which carried the host's requests as rxjs observables. That made rxjs part of the public API of a package that bundles its own copy of it, so a host would build bridges with a different rxjs than the one Element Call consumed them with — and asked every host to learn rxjs to change the theme. A component host now implements plain async callbacks for what Element Call tells it (`ElementCallHostBridge`, all optional), and makes its own requests through an imperative handle on the component's `ref` (`ElementCallHandle`: setTheme, join, hangUp, setDeviceMute), each resolving once Element Call has acted and rejecting when nothing in Element Call can. `component/host.ts` adapts that to the HostBridge the rest of Element Call still speaks, with a bridge whose identity never changes, so a host re-creating its callbacks on render restarts nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d18e82c546
commit
8e8bc5ddde
@@ -5,61 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { NEVER, Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
type DeviceMuteRequest,
|
||||
type DeviceMuteState,
|
||||
type ElementCallHostBridge,
|
||||
type HostRequest,
|
||||
} from "../index";
|
||||
import { type ElementCallHostBridge } from "../index";
|
||||
|
||||
/**
|
||||
* A host bridge that reports everything it is told and can be driven by hand,
|
||||
* so that the harness can watch both directions of the conversation between
|
||||
* Element Call and its host.
|
||||
* A host bridge that reports everything it is told, so that the harness can
|
||||
* watch what Element Call says to its host. (What the host says to Element
|
||||
* Call goes through the component's handle, and is logged by the pane.)
|
||||
*/
|
||||
export interface DevHostBridge extends ElementCallHostBridge {
|
||||
/** Tells Element Call the host has changed theme. */
|
||||
requestTheme(name: string): void;
|
||||
/** Tells Element Call to leave the call. */
|
||||
requestHangUp(): void;
|
||||
/** Asks Element Call to change, and report back, its mute state. */
|
||||
requestDeviceMute(request: DeviceMuteRequest): void;
|
||||
}
|
||||
|
||||
export function createDevHostBridge(
|
||||
log: (message: string) => void,
|
||||
/** What the host does when Element Call asks to be closed. */
|
||||
onClose: () => void,
|
||||
): DevHostBridge {
|
||||
const themeChange$ = new Subject<HostRequest<{ name?: string }>>();
|
||||
const hangUp$ = new Subject<HostRequest<Record<string, never>>>();
|
||||
const deviceMute$ = new Subject<
|
||||
HostRequest<DeviceMuteRequest, DeviceMuteState>
|
||||
>();
|
||||
|
||||
const ask = <Data, Reply>(
|
||||
subject: Subject<HostRequest<Data, Reply>>,
|
||||
name: string,
|
||||
data: Data,
|
||||
): void => {
|
||||
// Worth saying out loud: a request nobody is subscribed to is silently
|
||||
// dropped, and that is exactly the sort of thing the harness is for.
|
||||
if (!subject.observed) {
|
||||
log(`← ${name}: nothing is listening`);
|
||||
return;
|
||||
}
|
||||
log(`← ${name}`);
|
||||
subject.next({
|
||||
data,
|
||||
reply: (reply): void =>
|
||||
log(
|
||||
`→ ${name} acknowledged${reply === undefined ? "" : `: ${JSON.stringify(reply)}`}`,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
): ElementCallHostBridge {
|
||||
/**
|
||||
* Records something Element Call told the host. Nothing is sent anywhere, so
|
||||
* this is only asynchronous because a real host's answer would have to be.
|
||||
@@ -85,19 +42,5 @@ export function createDevHostBridge(
|
||||
await told("close");
|
||||
onClose();
|
||||
},
|
||||
|
||||
themeChange$,
|
||||
// The harness does not preload a call, so this is never asked for
|
||||
join$: NEVER,
|
||||
hangUp$,
|
||||
deviceMute$,
|
||||
|
||||
supportsReactions: true,
|
||||
|
||||
requestTheme: (name): void =>
|
||||
ask(themeChange$, `themeChange(${name})`, { name }),
|
||||
requestHangUp: (): void => ask(hangUp$, "hangUp", {}),
|
||||
requestDeviceMute: (request): void =>
|
||||
ask(deviceMute$, `deviceMute(${JSON.stringify(request)})`, request),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import { ElementCall } from "../index";
|
||||
import { ElementCall, type ElementCallHandle } from "../index";
|
||||
import { createDevHostBridge } from "./DevHostBridge";
|
||||
import { createSession, joinRoom } from "./session";
|
||||
import styles from "./Harness.module.css";
|
||||
@@ -102,6 +103,29 @@ const Pane: FC<{
|
||||
[log, session.label],
|
||||
);
|
||||
|
||||
// What the host asks of Element Call goes through the component's handle.
|
||||
// Worth saying out loud when a request is refused — asking to hang up when
|
||||
// there is no call, say — since that is the sort of thing the harness is for.
|
||||
const handle = useRef<ElementCallHandle>(null);
|
||||
const ask = (
|
||||
name: string,
|
||||
make: (handle: ElementCallHandle) => Promise<unknown>,
|
||||
): void => {
|
||||
if (handle.current === null) {
|
||||
log(session.label, `← ${name}: not mounted`);
|
||||
return;
|
||||
}
|
||||
log(session.label, `← ${name}`);
|
||||
make(handle.current).then(
|
||||
(reply) =>
|
||||
log(
|
||||
session.label,
|
||||
`→ ${name} acknowledged${reply === undefined ? "" : `: ${JSON.stringify(reply)}`}`,
|
||||
),
|
||||
(e: unknown) => log(session.label, `→ ${name} refused: ${e}`),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={styles.pane} data-testid="call-pane">
|
||||
<div className={styles.paneBar}>
|
||||
@@ -110,24 +134,42 @@ const Pane: FC<{
|
||||
<button onClick={(): void => setMounted((m) => !m)}>
|
||||
{mounted ? "Unmount" : "Mount"}
|
||||
</button>
|
||||
<button onClick={(): void => bridge.requestTheme("light")}>
|
||||
Light
|
||||
</button>
|
||||
<button onClick={(): void => bridge.requestTheme("dark")}>Dark</button>
|
||||
<button
|
||||
onClick={(): void =>
|
||||
bridge.requestDeviceMute({ audio_enabled: false })
|
||||
ask("setTheme(light)", async (h) => await h.setTheme("light"))
|
||||
}
|
||||
>
|
||||
Light
|
||||
</button>
|
||||
<button
|
||||
onClick={(): void =>
|
||||
ask("setTheme(dark)", async (h) => await h.setTheme("dark"))
|
||||
}
|
||||
>
|
||||
Dark
|
||||
</button>
|
||||
<button
|
||||
onClick={(): void =>
|
||||
ask(
|
||||
"setDeviceMute(audio: false)",
|
||||
async (h) => await h.setDeviceMute({ audio_enabled: false }),
|
||||
)
|
||||
}
|
||||
>
|
||||
Mute
|
||||
</button>
|
||||
<button onClick={(): void => bridge.requestHangUp()}>Hang up</button>
|
||||
<button
|
||||
onClick={(): void => ask("hangUp", async (h) => await h.hangUp())}
|
||||
>
|
||||
Hang up
|
||||
</button>
|
||||
</div>
|
||||
{/* Resizable, because how Element Call copes with the size it is given is
|
||||
one of the things we cannot find out from the standalone app */}
|
||||
<div className={styles.paneCall} data-testid="call-container">
|
||||
{mounted && (
|
||||
<ElementCall
|
||||
ref={handle}
|
||||
client={session.client}
|
||||
roomId={roomId}
|
||||
hostBridge={bridge}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { createRef } from "react";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import {
|
||||
type ElementCallHandle,
|
||||
type ElementCallHostBridge,
|
||||
useComponentHostBridge,
|
||||
} from "./host";
|
||||
|
||||
describe("useComponentHostBridge", () => {
|
||||
test("keeps one identity while the host supplies new objects", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ supplied }: { supplied: ElementCallHostBridge }) =>
|
||||
useComponentHostBridge(supplied, undefined),
|
||||
{ initialProps: { supplied: {} } },
|
||||
);
|
||||
const first = result.current;
|
||||
rerender({ supplied: { notifyJoined: async () => {} } });
|
||||
expect(result.current).toBe(first);
|
||||
});
|
||||
|
||||
test("forwards to whatever the host most recently supplied", async () => {
|
||||
const before = vi.fn().mockResolvedValue(undefined);
|
||||
const after = vi.fn().mockResolvedValue(undefined);
|
||||
const { result, rerender } = renderHook(
|
||||
({ supplied }: { supplied: ElementCallHostBridge }) =>
|
||||
useComponentHostBridge(supplied, undefined),
|
||||
{ initialProps: { supplied: { notifyJoined: before } } },
|
||||
);
|
||||
rerender({ supplied: { notifyJoined: after } });
|
||||
|
||||
await result.current.notifyJoined();
|
||||
expect(before).not.toHaveBeenCalled();
|
||||
expect(after).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("is quiet about what the host did not implement", async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useComponentHostBridge(undefined, undefined),
|
||||
);
|
||||
await expect(result.current.contentLoaded()).resolves.toBeUndefined();
|
||||
await expect(
|
||||
result.current.notifyDeviceMute({
|
||||
audio_enabled: true,
|
||||
video_enabled: false,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
expect(result.current.supportsReactions).toBe(true);
|
||||
});
|
||||
|
||||
test("only has a close when the host has one, since that is a signal", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ supplied }: { supplied: ElementCallHostBridge }) =>
|
||||
useComponentHostBridge(supplied, undefined),
|
||||
{ initialProps: { supplied: {} } },
|
||||
);
|
||||
expect(result.current.close).toBeUndefined();
|
||||
|
||||
const close = vi.fn().mockResolvedValue(undefined);
|
||||
rerender({ supplied: { close } });
|
||||
expect(result.current.close).toBeDefined();
|
||||
});
|
||||
|
||||
test("never offers profile changes, since the account is the host's", () => {
|
||||
const { result } = renderHook(() =>
|
||||
useComponentHostBridge(undefined, undefined),
|
||||
);
|
||||
expect(result.current.supportsProfileChanges).toBe(false);
|
||||
});
|
||||
|
||||
describe("the handle", () => {
|
||||
test("delivers a request to what is listening and resolves on its reply", async () => {
|
||||
const ref = createRef<ElementCallHandle>();
|
||||
const { result } = renderHook(() =>
|
||||
useComponentHostBridge(undefined, ref),
|
||||
);
|
||||
|
||||
const received = vi.fn();
|
||||
result.current.deviceMute$.subscribe(({ data, reply }) => {
|
||||
received(data);
|
||||
reply({ audio_enabled: data.audio_enabled!, video_enabled: true });
|
||||
});
|
||||
|
||||
await expect(
|
||||
ref.current!.setDeviceMute({ audio_enabled: false }),
|
||||
).resolves.toEqual({ audio_enabled: false, video_enabled: true });
|
||||
expect(received).toHaveBeenCalledWith({ audio_enabled: false });
|
||||
});
|
||||
|
||||
test("refuses a request nothing in Element Call is listening for", async () => {
|
||||
const ref = createRef<ElementCallHandle>();
|
||||
renderHook(() => useComponentHostBridge(undefined, ref));
|
||||
|
||||
await expect(ref.current!.hangUp()).rejects.toThrow(
|
||||
"Nothing in Element Call can hang up right now",
|
||||
);
|
||||
});
|
||||
|
||||
test("passes the theme name through", async () => {
|
||||
const ref = createRef<ElementCallHandle>();
|
||||
const { result } = renderHook(() =>
|
||||
useComponentHostBridge(undefined, ref),
|
||||
);
|
||||
const names: (string | undefined)[] = [];
|
||||
result.current.themeChange$.subscribe(({ data, reply }) => {
|
||||
names.push(data.name);
|
||||
reply();
|
||||
});
|
||||
|
||||
await ref.current!.setTheme("light");
|
||||
expect(names).toEqual(["light"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* How a host application and the Element Call component talk to each other.
|
||||
*
|
||||
* Inside Element Call the host is a {@link HostBridge}, which carries the
|
||||
* host's requests as rxjs observables because that is what the widget API and
|
||||
* the view models work in. A host should not have to know about rxjs, or agree
|
||||
* with us on a version of it, so a component host sees neither: it implements
|
||||
* plain async callbacks for what Element Call tells it, and makes its own
|
||||
* requests through an imperative handle on the component, the way it would
|
||||
* call `play()` on a video element. This module adapts the one to the other.
|
||||
*/
|
||||
|
||||
import { type Ref, useImperativeHandle } from "react";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
type DeviceMuteRequest,
|
||||
type DeviceMuteState,
|
||||
type HostBridge,
|
||||
type HostRequest,
|
||||
} from "../src/HostBridge";
|
||||
import { type JoinCallData } from "../src/widget";
|
||||
import { useInitial } from "../src/useInitial";
|
||||
import { useLatest } from "../src/useLatest";
|
||||
|
||||
/**
|
||||
* What Element Call tells the application embedding it. Everything is
|
||||
* optional: a host implements what it wants to hear about.
|
||||
*
|
||||
* Compared by nothing — Element Call always calls whichever one it was most
|
||||
* recently given, so this may be written inline.
|
||||
*/
|
||||
export interface ElementCallHostBridge {
|
||||
/**
|
||||
* Asks the host to keep Element Call on screen (or stop doing so), so that a
|
||||
* call in progress is not torn down when the user navigates elsewhere.
|
||||
*/
|
||||
setAlwaysOnScreen?(alwaysOnScreen: boolean): Promise<void>;
|
||||
/** Tells the host that Element Call has finished loading. */
|
||||
contentLoaded?(): Promise<void>;
|
||||
/** Tells the host that the user has joined the call. */
|
||||
notifyJoined?(): Promise<void>;
|
||||
/** Tells the host that the user has hung up. */
|
||||
notifyHungUp?(): Promise<void>;
|
||||
/** Tells the host the user's current audio and video mute state. */
|
||||
notifyDeviceMute?(state: DeviceMuteState): Promise<void>;
|
||||
/**
|
||||
* Asks the host to close Element Call: to unmount the component. Its
|
||||
* presence is what makes Element Call offer a close button on its error
|
||||
* screens, and leave the host to decide what is shown once a call has ended.
|
||||
* Without it, Element Call shows its own post-call screen, if it has one for
|
||||
* the situation, or nothing.
|
||||
*/
|
||||
close?(): Promise<void>;
|
||||
/**
|
||||
* Whether Element Call may send and receive reactions in this room.
|
||||
* Defaults to true.
|
||||
*/
|
||||
readonly supportsReactions?: boolean;
|
||||
/**
|
||||
* Fetches media on Element Call's behalf, for hosts that do not want it
|
||||
* touching the homeserver's media endpoints itself. Absent, Element Call
|
||||
* fetches media with the client it was given.
|
||||
*/
|
||||
downloadMedia?(mxcUri: string): Promise<Blob>;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a host can ask of a mounted Element Call, reached through the
|
||||
* component's `ref`. Each request resolves once Element Call has acted on it,
|
||||
* and rejects if nothing in Element Call is in a position to act: hanging up
|
||||
* when there is no call, say.
|
||||
*/
|
||||
export interface ElementCallHandle {
|
||||
/** Switches Element Call to the named theme, `light` or `dark`. */
|
||||
setTheme(name: string): Promise<void>;
|
||||
/**
|
||||
* Joins the call, when Element Call was configured to `preload` and is
|
||||
* waiting to be told to. Says which devices to join with.
|
||||
*/
|
||||
join(devices: JoinCallData): Promise<void>;
|
||||
/** Leaves the call. */
|
||||
hangUp(): Promise<void>;
|
||||
/**
|
||||
* Changes the mute state, for whichever of audio and video is given, and
|
||||
* reports the state that results.
|
||||
*/
|
||||
setDeviceMute(request: DeviceMuteRequest): Promise<DeviceMuteState>;
|
||||
}
|
||||
|
||||
/** Hands a request to Element Call and waits for it to be acknowledged. */
|
||||
async function request<Data, Reply>(
|
||||
listeners: Subject<HostRequest<Data, Reply>>,
|
||||
what: string,
|
||||
data: Data,
|
||||
): Promise<Reply> {
|
||||
if (!listeners.observed)
|
||||
throw new Error(`Nothing in Element Call can ${what} right now`);
|
||||
return await new Promise((resolve) =>
|
||||
listeners.next({ data, reply: resolve }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link HostBridge} the rest of Element Call sees, built from what a
|
||||
* component host supplies and wired to the handle it is given.
|
||||
*
|
||||
* The bridge is created once and never changes identity — everything that
|
||||
* depends on it would otherwise restart when the host re-rendered with a new
|
||||
* object — and forwards each call to whatever the host most recently passed.
|
||||
*/
|
||||
export function useComponentHostBridge(
|
||||
supplied: ElementCallHostBridge | undefined,
|
||||
ref: Ref<ElementCallHandle> | undefined,
|
||||
): HostBridge {
|
||||
const latest = useLatest(supplied ?? {});
|
||||
|
||||
const requests = useInitial(() => ({
|
||||
themeChange$: new Subject<HostRequest<{ name?: string }>>(),
|
||||
join$: new Subject<HostRequest<JoinCallData>>(),
|
||||
hangUp$: new Subject<HostRequest<Record<string, never>>>(),
|
||||
deviceMute$: new Subject<HostRequest<DeviceMuteRequest, DeviceMuteState>>(),
|
||||
}));
|
||||
|
||||
const bridge = useInitial(
|
||||
(): HostBridge => ({
|
||||
setAlwaysOnScreen: async (alwaysOnScreen) => {
|
||||
await latest.current.setAlwaysOnScreen?.(alwaysOnScreen);
|
||||
},
|
||||
contentLoaded: async () => {
|
||||
await latest.current.contentLoaded?.();
|
||||
},
|
||||
notifyJoined: async () => {
|
||||
await latest.current.notifyJoined?.();
|
||||
},
|
||||
notifyHungUp: async () => {
|
||||
await latest.current.notifyHungUp?.();
|
||||
},
|
||||
notifyDeviceMute: async (state) => {
|
||||
await latest.current.notifyDeviceMute?.(state);
|
||||
},
|
||||
// Whether these exist is itself information, so they are read through
|
||||
// rather than wrapped unconditionally
|
||||
get close() {
|
||||
const close = latest.current.close;
|
||||
return close === undefined
|
||||
? undefined
|
||||
: async (): Promise<void> => await close();
|
||||
},
|
||||
get downloadMedia() {
|
||||
const downloadMedia = latest.current.downloadMedia;
|
||||
return downloadMedia === undefined
|
||||
? undefined
|
||||
: async (mxcUri: string): Promise<Blob> =>
|
||||
await downloadMedia(mxcUri);
|
||||
},
|
||||
get supportsReactions(): boolean {
|
||||
return latest.current.supportsReactions ?? true;
|
||||
},
|
||||
// Whatever the host says or does not say, the account is its own: it
|
||||
// signed the user in and handed us the client. So Element Call never
|
||||
// offers to edit the profile from inside a component.
|
||||
supportsProfileChanges: false,
|
||||
...requests,
|
||||
}),
|
||||
);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): ElementCallHandle => ({
|
||||
setTheme: async (name) =>
|
||||
await request(requests.themeChange$, "change theme", { name }),
|
||||
join: async (devices) =>
|
||||
await request(requests.join$, "join a call", devices),
|
||||
hangUp: async () => await request(requests.hangUp$, "hang up", {}),
|
||||
setDeviceMute: async (muteRequest) =>
|
||||
await request(
|
||||
requests.deviceMute$,
|
||||
"change the mute state",
|
||||
muteRequest,
|
||||
),
|
||||
}),
|
||||
[requests],
|
||||
);
|
||||
|
||||
return bridge;
|
||||
}
|
||||
+20
-30
@@ -36,6 +36,7 @@ import {
|
||||
type FC,
|
||||
type JSX,
|
||||
type ReactNode,
|
||||
type Ref,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
@@ -53,11 +54,7 @@ import EN from "../locales/en/app.json";
|
||||
import { ElementCallView } from "../src/ElementCallView";
|
||||
import { ErrorPage } from "../src/FullScreenView";
|
||||
import { ClientProvider } from "../src/ClientContext";
|
||||
import {
|
||||
type HostBridge,
|
||||
HostBridgeProvider,
|
||||
nullHostBridge,
|
||||
} from "../src/HostBridge";
|
||||
import { HostBridgeProvider } from "../src/HostBridge";
|
||||
import { RootElementProvider } from "../src/RootElementContext";
|
||||
import {
|
||||
configurationForIntent,
|
||||
@@ -76,13 +73,17 @@ import { i18n } from "../src/utils/i18n";
|
||||
import { useTheme } from "../src/useTheme";
|
||||
import { useStableValue } from "../src/useStableValue";
|
||||
import styles from "./ElementCall.module.css";
|
||||
import {
|
||||
type ElementCallHandle,
|
||||
type ElementCallHostBridge,
|
||||
useComponentHostBridge,
|
||||
} from "./host";
|
||||
|
||||
// Everything needed to implement a HostBridge, not just the interface itself
|
||||
// How the host and Element Call talk to each other, and what they say
|
||||
export { type ElementCallHandle, type ElementCallHostBridge } from "./host";
|
||||
export {
|
||||
type DeviceMuteRequest,
|
||||
type DeviceMuteState,
|
||||
type HostBridge,
|
||||
type HostRequest,
|
||||
} from "../src/HostBridge";
|
||||
export { type JoinCallData } from "../src/widget";
|
||||
// The deployment-wide configuration, as distinct from ElementCallConfiguration
|
||||
@@ -102,14 +103,6 @@ export {
|
||||
*/
|
||||
export type ElementCallConfiguration = Partial<UrlParams>;
|
||||
|
||||
/**
|
||||
* What a host embedding Element Call implements to talk to it. This is the
|
||||
* {@link HostBridge} less what Element Call already knows about such a host:
|
||||
* the account is the host's, since the client is, so the profile is not
|
||||
* Element Call's to change.
|
||||
*/
|
||||
export type ElementCallHostBridge = Omit<HostBridge, "supportsProfileChanges">;
|
||||
|
||||
export interface ElementCallProps {
|
||||
/**
|
||||
* The client to place the call with. Element Call does not authenticate
|
||||
@@ -138,11 +131,16 @@ export interface ElementCallProps {
|
||||
*/
|
||||
config?: ElementCallConfiguration;
|
||||
/**
|
||||
* How to reach the host while the call is running — to be told the user has
|
||||
* joined or hung up, to be asked to keep the call on screen, and so on.
|
||||
* Without one, Element Call assumes it has no host to talk to.
|
||||
* What Element Call tells the host while the call is running: that the user
|
||||
* has joined or hung up, that it would like to be kept on screen, and so on.
|
||||
* Without one, Element Call assumes nobody is listening.
|
||||
*/
|
||||
hostBridge?: ElementCallHostBridge;
|
||||
/**
|
||||
* What the host tells Element Call: to change theme, to hang up, to mute.
|
||||
* Available once the component has rendered.
|
||||
*/
|
||||
ref?: Ref<ElementCallHandle>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,18 +189,10 @@ export const ElementCall: FC<ElementCallProps> = ({
|
||||
roomId,
|
||||
intent = UserIntent.JoinExistingCall,
|
||||
config,
|
||||
hostBridge: suppliedHostBridge = nullHostBridge,
|
||||
hostBridge: suppliedHostBridge,
|
||||
ref,
|
||||
}): ReactNode => {
|
||||
// Whatever the host says or does not say, the account is its own: it signed
|
||||
// the user in and handed us the client. So Element Call never offers to edit
|
||||
// the profile from inside a component.
|
||||
const hostBridge = useMemo(
|
||||
(): HostBridge => ({
|
||||
...suppliedHostBridge,
|
||||
supportsProfileChanges: false,
|
||||
}),
|
||||
[suppliedHostBridge],
|
||||
);
|
||||
const hostBridge = useComponentHostBridge(suppliedHostBridge, ref);
|
||||
|
||||
// The container is what Element Call decorates and portals into, so nothing
|
||||
// inside can render until we have it.
|
||||
|
||||
Reference in New Issue
Block a user