From 8ebb0b5a908a5611476ff4aef1c767ab36eac763 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Sat, 19 Sep 2026 08:38:47 +0200 Subject: [PATCH] Export component related types. --- README.md | 29 +++- component/api.ts | 124 +++++++++++++++++ component/index.tsx | 102 +------------- component/package.json | 4 + component/tsconfig.build.json | 6 +- docs/agents/architecture.md | 5 +- src/UrlConfiguration.ts | 242 ++++++++++++++++++++++++++++++++++ src/UrlParams.ts | 239 +++------------------------------ vite-component.config.ts | 15 ++- 9 files changed, 436 insertions(+), 330 deletions(-) create mode 100644 component/api.ts create mode 100644 src/UrlConfiguration.ts diff --git a/README.md b/README.md index 84910ed57..e7f9a62d7 100644 --- a/README.md +++ b/README.md @@ -34,16 +34,16 @@ You can find the latest development version continuously deployed to ## ✨ Key Features ✅ **Decentralized & Federated** – No central authority; works across Matrix -homeservers. -✅ **End-to-End Encrypted** – Secure and private calls. +homeservers. +✅ **End-to-End Encrypted** – Secure and private calls. ✅ **Standalone, Widget & Component Mode** – Use as an independent app, embed in Matrix clients as a widget, or (experimentally) mount it as a React component -inside your own application. -✅ **WebRTC-based** – No additional software required. +inside your own application. +✅ **WebRTC-based** – No additional software required. ✅ **Scalable with LiveKit** – Supports large meetings via SFU -([MSC4195: MatrixRTC using LiveKit backend](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)). +([MSC4195: MatrixRTC using LiveKit backend](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)). ✅ **Raise Hand** – Participants can signal when they want to speak, helping to -organize the flow of the meeting. +organize the flow of the meeting. ✅ **Emoji Reactions** – Users can react with emojis 👍️ 🎉 👏 🤘, adding engagement and interactivity to the conversation. @@ -309,6 +309,23 @@ root. The host imports the component from `react-dom`, `matrix-js-sdk` and `livekit-client` itself, since the bundle leaves them external. +The component is large, and a host will usually load it lazily, only once a +call is shown. Everything a host needs in order to talk about a call before +then is also exported +from `@element-hq/element-call-component/api` (a small entry point that does not +load the component). + +- props and the types they are made of +- the handle +- the host bridge +- `UserIntent` +- `HeaderStyle` +- `BackgroundStyle` enums +- `configurationForIntent`, the defaults each intent implies + Import from it wherever a value such as + `BackgroundStyle.Solid` is needed on a path that must stay light; the main + entry point re-exports all of it too. + ### Backend A docker compose file `docker-compose-dev.yml` is provided to start the diff --git a/component/api.ts b/component/api.ts new file mode 100644 index 000000000..c9e7a2fd7 --- /dev/null +++ b/component/api.ts @@ -0,0 +1,124 @@ +/* +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. +*/ + +/** + * EXPERIMENTAL + * + * What a host needs in order to talk about Element Call, without Element Call: + * the component's props and the types they are made of, the handle and the + * host bridge, the intents a call can be started with and the configuration + * each implies. + * + * This is an entry point of its own, `@element-hq/element-call-component/api`, + * so that a host can import it on a path that must stay light — a call model + * that needs `BackgroundStyle.Solid` as a value, say — and load the component + * itself, and everything it brings, only when a call is shown. The main entry + * point re-exports all of it, so a host that does not care may import + * everything from there. + * + * Nothing in here may reach anything that is not a type or a constant: the + * point of this module is what it does not import. + */ + +import type { Ref } from "react"; +import type { MatrixClient } from "matrix-js-sdk"; + +import type { UrlProperties } from "../src/UrlParams"; +import type { ElementCallHandle, ElementCallHostBridge } from "./host"; +import { + BackgroundStyle, + configurationForIntent, + HeaderStyle, + type UrlConfiguration, + UserIntent, +} from "../src/UrlConfiguration"; + +// How the host and Element Call talk to each other, and what they say +export type { ElementCallHandle, ElementCallHostBridge } from "./host"; +export type { DeviceMuteRequest, DeviceMuteState } from "../src/HostBridge"; +export type { JoinCallData } from "../src/widget"; +// The deployment-wide configuration, as distinct from ElementCallConfiguration +// below, which is per call +export type { ConfigOptions } from "../src/config/ConfigOptions"; +// The values that appear in ElementCallConfiguration and in the intent, and +// what each intent means by default +export { + BackgroundStyle, + configurationForIntent, + HeaderStyle, + type UrlConfiguration, + UserIntent, +}; + +/** + * How Element Call should behave. Everything is optional; anything left out + * takes the default that {@link ElementCallProps.intent} implies. + * + * This is the behaviour a widget can be configured with through its URL, plus + * the one fact about the call a host has a say in here, the background. The + * rest of what a widget's URL carries — who the user is, how to reach the + * homeserver, where to report analytics, the shared secret of a room that is + * encrypted with one — a component host supplies by other routes, or not at + * all; and what can change while the call is running, the theme and the + * language, is a prop of its own. + */ +export type ElementCallConfiguration = Partial & + Partial>; + +export interface ElementCallProps { + /** + * The client to place the call with. Element Call does not authenticate + * anyone or manage a session of its own; this one is the host's. + */ + client: MatrixClient; + /** The room to call in. The host's client must already know about it. */ + roomId: string; + /** + * What the user asked for — whether they started the call or joined one that + * was already running, and whether it is a call in a group or a DM. Element + * Call decides what each of those means: whether to show the lobby first, + * whether to ring, and so on. + * + * Defaults to joining an existing group call, which is the most conservative + * reading, but a host that knows which button the user pressed should say so. + */ + intent?: UserIntent; + /** + * How Element Call should behave, overriding whatever {@link intent} implies. + * A host that finds itself setting a lot of these probably wants a different + * intent instead. + * + * Compared by value, so it is fine to write this inline; only a change to + * what it says restarts anything. + */ + config?: ElementCallConfiguration; + /** + * 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 hang up, to mute, to join. Available + * once the component has rendered. + */ + ref?: Ref; + /** + * The theme to show Element Call in, `light` or `dark`. Left out, Element + * Call picks. Changes take effect at once, and cost nothing else. + */ + theme?: string; + /** + * The language to show Element Call in, as a BCP 47 tag: one of the + * `supportedLanguages` the main entry point exports, or something that falls + * back to one (`de-AT` to `de`). Left out, the browser's language is used. + * + * Translations are one thing shared by every Element Call on the page, so + * the most recently set language wins for all of them. + */ + language?: string; +} diff --git a/component/index.tsx b/component/index.tsx index b3f04e00c..128f4a55e 100644 --- a/component/index.tsx +++ b/component/index.tsx @@ -36,14 +36,12 @@ import { type FC, type JSX, type ReactNode, - type Ref, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react"; -import { type MatrixClient } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/lib/logger"; import { I18nextProvider } from "react-i18next"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -62,10 +60,8 @@ import { RootElementProvider, useRootElement } from "../src/RootElementContext"; import { configurationForIntent, componentProperties, - type UrlConfiguration, type UrlParams, UrlParamsProvider, - type UrlProperties, UserIntent, useUrlParams, } from "../src/UrlParams"; @@ -79,102 +75,16 @@ 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"; +import { useComponentHostBridge } from "./host"; +import { type ElementCallProps } from "./api"; import { supportedLanguages, translationsBackend } from "./localization"; // The languages Element Call can be shown in export { supportedLanguages } from "./localization"; - -// 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, -} from "../src/HostBridge"; -export { type JoinCallData } from "../src/widget"; -// The deployment-wide configuration, as distinct from ElementCallConfiguration -// above, which is per call -export { type ConfigOptions } from "../src/config/ConfigOptions"; -// The values that appear in ElementCallConfiguration and in the intent -export { - BackgroundStyle, - HeaderStyle, - UserIntent, - type UrlConfiguration, -} from "../src/UrlParams"; - -/** - * How Element Call should behave. Everything is optional; anything left out - * takes the default that {@link ElementCallProps.intent} implies. - * - * This is the behaviour a widget can be configured with through its URL, plus - * the one fact about the call a host has a say in here, the background. The - * rest of what a widget's URL carries — who the user is, how to reach the - * homeserver, where to report analytics, the shared secret of a room that is - * encrypted with one — a component host supplies by other routes, or not at - * all; and what can change while the call is running, the theme and the - * language, is a prop of its own. - */ -export type ElementCallConfiguration = Partial & - Partial>; - -export interface ElementCallProps { - /** - * The client to place the call with. Element Call does not authenticate - * anyone or manage a session of its own; this one is the host's. - */ - client: MatrixClient; - /** The room to call in. The host's client must already know about it. */ - roomId: string; - /** - * What the user asked for — whether they started the call or joined one that - * was already running, and whether it is a call in a group or a DM. Element - * Call decides what each of those means: whether to show the lobby first, - * whether to ring, and so on. - * - * Defaults to joining an existing group call, which is the most conservative - * reading, but a host that knows which button the user pressed should say so. - */ - intent?: UserIntent; - /** - * How Element Call should behave, overriding whatever {@link intent} implies. - * A host that finds itself setting a lot of these probably wants a different - * intent instead. - * - * Compared by value, so it is fine to write this inline; only a change to - * what it says restarts anything. - */ - config?: ElementCallConfiguration; - /** - * 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 hang up, to mute, to join. Available - * once the component has rendered. - */ - ref?: Ref; - /** - * The theme to show Element Call in, `light` or `dark`. Left out, Element - * Call picks. Changes take effect at once, and cost nothing else. - */ - theme?: string; - /** - * The language to show Element Call in, as a BCP 47 tag: one of - * {@link supportedLanguages}, or something that falls back to one (`de-AT` - * to `de`). Left out, the browser's language is used. - * - * Translations are one thing shared by every Element Call on the page, so - * the most recently set language wins for all of them. - */ - language?: string; -} +// Everything a host needs to talk about Element Call — the props, the handle, +// the host bridge, the intents and what each means — also available without +// the component itself from `@element-hq/element-call-component/api` +export * from "./api"; /** * Prepares the things Element Call needs before it can be shown: translations, diff --git a/component/package.json b/component/package.json index 9ed81e02a..79aa286c5 100644 --- a/component/package.json +++ b/component/package.json @@ -25,6 +25,10 @@ "types": "./dist/types/component/index.d.ts", "default": "./dist/element-call.js" }, + "./api": { + "types": "./dist/types/component/api.d.ts", + "default": "./dist/api.js" + }, "./style.css": "./dist/element-call.css" }, "sideEffects": [ diff --git a/component/tsconfig.build.json b/component/tsconfig.build.json index 18af65d84..cdbe57531 100644 --- a/component/tsconfig.build.json +++ b/component/tsconfig.build.json @@ -13,8 +13,8 @@ "rootDir": "..", "outDir": "./dist/types" }, - // The entry point, plus the ambient declarations (CSS modules, `?react` SVGs, - // `import.meta.env`, …) that the sources it reaches rely on. - "include": ["./index.tsx", "../src/@types/*.d.ts"], + // The entry points, plus the ambient declarations (CSS modules, `?react` SVGs, + // `import.meta.env`, …) that the sources they reach rely on. + "include": ["./index.tsx", "./api.ts", "../src/@types/*.d.ts"], "exclude": [] } diff --git a/docs/agents/architecture.md b/docs/agents/architecture.md index 92cac2575..bd00bf325 100644 --- a/docs/agents/architecture.md +++ b/docs/agents/architecture.md @@ -76,4 +76,7 @@ Code that builds in only one is a bug. - `build:component` — `@element-hq/element-call-component`, sources in `component/` (its own pnpm project; run pnpm from the repo root). Host API is in the README; `pnpm lint:externals` rejects an import of a `react` / `react-dom` / - `matrix-js-sdk` / `livekit-client` subpath the externals list omits. + `matrix-js-sdk` / `livekit-client` subpath the externals list omits. Two entry + points: `index.tsx` (the component) and `api.ts` (types, enums and + `configurationForIntent`, for a host that must not load the component). `api.ts` + may only reach types and constants — `src/UrlConfiguration.ts`, not `UrlParams`. diff --git a/src/UrlConfiguration.ts b/src/UrlConfiguration.ts new file mode 100644 index 000000000..6de204bb9 --- /dev/null +++ b/src/UrlConfiguration.ts @@ -0,0 +1,242 @@ +/* +Copyright 2022-2024 New Vector Ltd. +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 Element Call can be told to behave, and what each intent means. + * + * Kept apart from {@link UrlParams} on purpose: everything here is constants, + * types and one function of them, so that a host of the component can import + * these — the enums it needs as values, the defaults an intent implies — without + * loading Element Call itself. UrlParams, which reads them from a URL, needs + * the router, the config and the rest of the app; this module must not. + * `component/api.ts` is built from it as an entry point of its own. + */ + +import { + type RTCCallIntent, + type RTCNotificationType, +} from "matrix-js-sdk/lib/matrixrtc"; + +import { platform } from "./Platform"; + +export enum UserIntent { + StartNewCall = "start_call", + JoinExistingCall = "join_existing", + StartNewCallVoice = "start_call_voice", + JoinExistingCallVoice = "join_existing_voice", + StartNewCallDM = "start_call_dm", + StartNewCallDMVoice = "start_call_dm_voice", + JoinExistingCallDM = "join_existing_dm", + JoinExistingCallDMVoice = "join_existing_dm_voice", + Unknown = "unknown", +} + +export enum HeaderStyle { + None = "none", + Standard = "standard", + AppBar = "app_bar", +} + +export enum BackgroundStyle { + Solid = "solid", + Gradient = "gradient", +} + +/** + * The configuration for the app, which can be set via URL parameters. + * Those property are different to the UrlProperties, since they are all optional + * and configure the behavior of the app. Their value is the same if EC is used in + * the same context but with different accounts/users. + * + * Their defaults can be controlled by the `intent` property. + */ +export interface UrlConfiguration { + /** + * Whether the app should keep the user confined to the current call/room. + */ + confineToRoom: boolean; + /** + * Whether the app should pause before joining the call until it sees an + * io.element.join widget action, allowing it to be preloaded. + */ + preload: boolean; + /** + * The style of headers to show. "standard" is the default arrangement, "none" + * hides the header entirely, and "app_bar" produces a header with a back + * button like you might see in mobile apps. The callback for the back button + * is window.controls.onBackButtonPressed. + */ + header: HeaderStyle; + /** + * Whether the controls should be shown. For screen recording no controls can be desired. + */ + showControls: boolean; + /** + * Whether to hide the screen-sharing button. + */ + hideScreensharing: boolean; + + /** + * Whether the app is allowed to use fallback STUN servers for ICE in case the + * user's homeserver doesn't provide any. + */ + allowIceFallback: boolean; + + /** + * Whether the app should use per participant keys for E2EE. + */ + perParticipantE2EE: boolean; + /** + * Whether the global JS controls for audio output devices should be enabled, + * allowing the list of output devices to be controlled by the app hosting + * Element Call. + */ + controlledAudioDevices: boolean; + /** + * Setting this flag skips the lobby and brings you in the call directly. + * In the widget this can be combined with preload to pass the device settings + * with the join widget action. + */ + skipLobby: boolean; + /** + * Setting this flag makes element call show the lobby after leaving a call. + * This is useful for video rooms. + */ + returnToLobby: boolean; + /** + * Whether and what type of notification EC should send, when the user joins the call. + */ + sendNotificationType?: RTCNotificationType; + /** + * Whether the app should automatically leave the call when there + * is no one left in the call. + * This is one part to make the call matrixRTC session behave like a telephone call. + */ + autoLeaveWhenOthersLeft: boolean; + + /** + * If the client should behave like it is awaiting an answer if a notification was sent (wait for call pick up). + * This is a no-op if not combined with sendNotificationType. + * + * This entails: + * - show ui that it is awaiting an answer + * - play a sound that indicates that it is awaiting an answer + * - auto-dismiss the call widget once the notification lifetime expires on the receivers side. + */ + waitForCallPickup: boolean; + + /** + * Whether to enable echo cancellation for audio capture. + * Defaults to true. + */ + echoCancellation?: boolean; + /** + * Whether to enable noise suppression for audio capture. + * Defaults to true. + */ + noiseSuppression?: boolean; + + callIntent?: RTCCallIntent; +} + +// If you need to add a new flag to this interface, prefer a name that describes +// a specific behavior (such as 'confineToRoom'), rather than one that describes +// the situations that call for this behavior ('isEmbedded'). This makes it +// clearer what each flag means, and helps us avoid coupling Element Call's +// behavior to the needs of specific consumers. + +/** + * The configuration implied by what the user meant to do — if they pressed a + * Start Call button this would be `start_call`, and if they pressed Join Call, + * `join_existing`. + * + * These are platform-specific defaults, so that a host can start a call by + * saying what the user asked for rather than by setting every parameter itself, + * and so that what each intent means is Element Call's decision, made in one + * place. A host that wants something else states it alongside the intent. + * + * {@link UserIntent.Unknown} means no intent was stated, and gives the + * standalone app's defaults: Element Call owns the whole page, so it offers the + * way out of the room that a hosted call must not. + */ +export function configurationForIntent(intent: UserIntent): UrlConfiguration { + // Only constants and `platform` here, so that this depends on nothing but + // the intent. + let preset: UrlConfiguration = { + confineToRoom: true, + preload: false, + header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar, + showControls: true, + hideScreensharing: false, + allowIceFallback: true, + perParticipantE2EE: true, + controlledAudioDevices: platform === "desktop" ? false : true, + skipLobby: true, + returnToLobby: false, + sendNotificationType: "notification", + autoLeaveWhenOthersLeft: false, + waitForCallPickup: false, + }; + switch (intent) { + case UserIntent.StartNewCall: + preset.skipLobby = false; + preset.callIntent = "video"; + break; + case UserIntent.JoinExistingCall: + // On desktop this will be overridden based on which button was used to join the call + preset.skipLobby = false; + preset.callIntent = "video"; + break; + case UserIntent.StartNewCallVoice: + preset.skipLobby = false; + preset.callIntent = "audio"; + break; + case UserIntent.JoinExistingCallVoice: + // On desktop this will be overridden based on which button was used to join the call + preset.skipLobby = false; + preset.callIntent = "audio"; + break; + case UserIntent.StartNewCallDMVoice: + preset.callIntent = "audio"; + // Fall through + case UserIntent.StartNewCallDM: + preset.skipLobby = true; + preset.sendNotificationType = "ring"; + preset.autoLeaveWhenOthersLeft = true; + preset.waitForCallPickup = true; + preset.callIntent = preset.callIntent ?? "video"; + break; + case UserIntent.JoinExistingCallDMVoice: + preset.callIntent = "audio"; + // Fall through + case UserIntent.JoinExistingCallDM: + // On desktop this will be overridden based on which button was used to join the call + preset.skipLobby = true; + preset.autoLeaveWhenOthersLeft = true; + preset.callIntent = preset.callIntent ?? "video"; + break; + // Non widget usecase defaults + default: + preset = { + confineToRoom: false, + preload: false, + header: HeaderStyle.Standard, + showControls: true, + hideScreensharing: false, + allowIceFallback: false, + perParticipantE2EE: false, + controlledAudioDevices: false, + skipLobby: false, + returnToLobby: false, + sendNotificationType: undefined, + autoLeaveWhenOthersLeft: false, + waitForCallPickup: false, + }; + } + return preset; +} diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 7c32c15f8..9d7ee3001 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -9,47 +9,36 @@ Please see LICENSE in the repository root for full details. import { createContext, use, useMemo } from "react"; import { useLocation } from "react-router-dom"; import { logger } from "matrix-js-sdk/lib/logger"; -import { - type RTCCallIntent, - type RTCNotificationType, -} from "matrix-js-sdk/lib/matrixrtc"; import { pickBy } from "lodash-es"; import { Config } from "./config/Config"; import { type EncryptionSystem } from "./e2ee/sharedKeyManagement"; import { E2eeType } from "./e2ee/e2eeType"; -import { platform } from "./Platform"; +import { + BackgroundStyle, + configurationForIntent, + HeaderStyle, + type UrlConfiguration, + UserIntent, +} from "./UrlConfiguration"; import { redact } from "./utils/redact"; +// Moved to their own module, so that a host can import them without the rest +// of this one; re-exported here as they always were +export { + BackgroundStyle, + configurationForIntent, + HeaderStyle, + type UrlConfiguration, + UserIntent, +} from "./UrlConfiguration"; + interface RoomIdentifier { roomAlias: string | null; roomId: string | null; viaServers: string[]; } -export enum UserIntent { - StartNewCall = "start_call", - JoinExistingCall = "join_existing", - StartNewCallVoice = "start_call_voice", - JoinExistingCallVoice = "join_existing_voice", - StartNewCallDM = "start_call_dm", - StartNewCallDMVoice = "start_call_dm_voice", - JoinExistingCallDM = "join_existing_dm", - JoinExistingCallDMVoice = "join_existing_dm_voice", - Unknown = "unknown", -} - -export enum HeaderStyle { - None = "none", - Standard = "standard", - AppBar = "app_bar", -} - -export enum BackgroundStyle { - Solid = "solid", - Gradient = "gradient", -} - /** * The UrlProperties are used to pass required data to the widget. * Those are different in different rooms, users, devices. They do not configure the behavior of the @@ -166,109 +155,6 @@ export interface UrlProperties { */ background: BackgroundStyle; } - -/** - * The configuration for the app, which can be set via URL parameters. - * Those property are different to the UrlProperties, since they are all optional - * and configure the behavior of the app. Their value is the same if EC is used in - * the same context but with different accounts/users. - * - * Their defaults can be controlled by the `intent` property. - */ -export interface UrlConfiguration { - /** - * Whether the app should keep the user confined to the current call/room. - */ - confineToRoom: boolean; - /** - * Whether the app should pause before joining the call until it sees an - * io.element.join widget action, allowing it to be preloaded. - */ - preload: boolean; - /** - * The style of headers to show. "standard" is the default arrangement, "none" - * hides the header entirely, and "app_bar" produces a header with a back - * button like you might see in mobile apps. The callback for the back button - * is window.controls.onBackButtonPressed. - */ - header: HeaderStyle; - /** - * Whether the controls should be shown. For screen recording no controls can be desired. - */ - showControls: boolean; - /** - * Whether to hide the screen-sharing button. - */ - hideScreensharing: boolean; - - /** - * Whether the app is allowed to use fallback STUN servers for ICE in case the - * user's homeserver doesn't provide any. - */ - allowIceFallback: boolean; - - /** - * Whether the app should use per participant keys for E2EE. - */ - perParticipantE2EE: boolean; - /** - * Whether the global JS controls for audio output devices should be enabled, - * allowing the list of output devices to be controlled by the app hosting - * Element Call. - */ - controlledAudioDevices: boolean; - /** - * Setting this flag skips the lobby and brings you in the call directly. - * In the widget this can be combined with preload to pass the device settings - * with the join widget action. - */ - skipLobby: boolean; - /** - * Setting this flag makes element call show the lobby after leaving a call. - * This is useful for video rooms. - */ - returnToLobby: boolean; - /** - * Whether and what type of notification EC should send, when the user joins the call. - */ - sendNotificationType?: RTCNotificationType; - /** - * Whether the app should automatically leave the call when there - * is no one left in the call. - * This is one part to make the call matrixRTC session behave like a telephone call. - */ - autoLeaveWhenOthersLeft: boolean; - - /** - * If the client should behave like it is awaiting an answer if a notification was sent (wait for call pick up). - * This is a no-op if not combined with sendNotificationType. - * - * This entails: - * - show ui that it is awaiting an answer - * - play a sound that indicates that it is awaiting an answer - * - auto-dismiss the call widget once the notification lifetime expires on the receivers side. - */ - waitForCallPickup: boolean; - - /** - * Whether to enable echo cancellation for audio capture. - * Defaults to true. - */ - echoCancellation?: boolean; - /** - * Whether to enable noise suppression for audio capture. - * Defaults to true. - */ - noiseSuppression?: boolean; - - callIntent?: RTCCallIntent; -} - -// If you need to add a new flag to this interface, prefer a name that describes -// a specific behavior (such as 'confineToRoom'), rather than one that describes -// the situations that call for this behavior ('isEmbedded'). This makes it -// clearer what each flag means, and helps us avoid coupling Element Call's -// behavior to the needs of specific consumers. export interface UrlParams extends UrlProperties, UrlConfiguration {} class ParamParser { @@ -354,97 +240,6 @@ export const getUrlParams = ( return params; }; -/** - * The configuration implied by what the user meant to do — if they pressed a - * Start Call button this would be `start_call`, and if they pressed Join Call, - * `join_existing`. - * - * These are platform-specific defaults, so that a host can start a call by - * saying what the user asked for rather than by setting every parameter itself, - * and so that what each intent means is Element Call's decision, made in one - * place. A host that wants something else states it alongside the intent. - * - * {@link UserIntent.Unknown} means no intent was stated, and gives the - * standalone app's defaults: Element Call owns the whole page, so it offers the - * way out of the room that a hosted call must not. - */ -export function configurationForIntent(intent: UserIntent): UrlConfiguration { - // Only constants and `platform` here, so that this depends on nothing but - // the intent. - let preset: UrlConfiguration = { - confineToRoom: true, - preload: false, - header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar, - showControls: true, - hideScreensharing: false, - allowIceFallback: true, - perParticipantE2EE: true, - controlledAudioDevices: platform === "desktop" ? false : true, - skipLobby: true, - returnToLobby: false, - sendNotificationType: "notification", - autoLeaveWhenOthersLeft: false, - waitForCallPickup: false, - }; - switch (intent) { - case UserIntent.StartNewCall: - preset.skipLobby = false; - preset.callIntent = "video"; - break; - case UserIntent.JoinExistingCall: - // On desktop this will be overridden based on which button was used to join the call - preset.skipLobby = false; - preset.callIntent = "video"; - break; - case UserIntent.StartNewCallVoice: - preset.skipLobby = false; - preset.callIntent = "audio"; - break; - case UserIntent.JoinExistingCallVoice: - // On desktop this will be overridden based on which button was used to join the call - preset.skipLobby = false; - preset.callIntent = "audio"; - break; - case UserIntent.StartNewCallDMVoice: - preset.callIntent = "audio"; - // Fall through - case UserIntent.StartNewCallDM: - preset.skipLobby = true; - preset.sendNotificationType = "ring"; - preset.autoLeaveWhenOthersLeft = true; - preset.waitForCallPickup = true; - preset.callIntent = preset.callIntent ?? "video"; - break; - case UserIntent.JoinExistingCallDMVoice: - preset.callIntent = "audio"; - // Fall through - case UserIntent.JoinExistingCallDM: - // On desktop this will be overridden based on which button was used to join the call - preset.skipLobby = true; - preset.autoLeaveWhenOthersLeft = true; - preset.callIntent = preset.callIntent ?? "video"; - break; - // Non widget usecase defaults - default: - preset = { - confineToRoom: false, - preload: false, - header: HeaderStyle.Standard, - showControls: true, - hideScreensharing: false, - allowIceFallback: false, - perParticipantE2EE: false, - controlledAudioDevices: false, - skipLobby: false, - returnToLobby: false, - sendNotificationType: undefined, - autoLeaveWhenOthersLeft: false, - waitForCallPickup: false, - }; - } - return preset; -} - /** * The {@link UrlProperties} for Element Call running as a component inside a * host application. diff --git a/vite-component.config.ts b/vite-component.config.ts index ae0ac1508..a2f26c214 100644 --- a/vite-component.config.ts +++ b/vite-component.config.ts @@ -49,8 +49,19 @@ export default defineConfig(({ mode }) => { cssCodeSplit: false, lib: { formats: ["es" as const], - entry: "./component/index.tsx", - fileName: "element-call", + entry: { + // The component + "element-call": "./component/index.tsx", + // What a host needs to talk about it, without it (see component/api.ts). + // Built as an entry point of its own so that importing it does not + // load the component: the bundler gives it a chunk that reaches only + // what it imports. + api: "./component/api.ts", + }, + fileName: (_format, entryName) => `${entryName}.js`, + // The one stylesheet (see cssCodeSplit) keeps the name it had when the + // component was the only entry + cssFileName: "element-call", }, rollupOptions: { // The host already has these, and a second copy of any of them does not