Say "component" for the component

"Embedded" already means something here: the embedded package is the
widget build. Where a comment meant Element Call running as a React
component inside a host application, it now says so, and the params it
starts from are `componentProperties`. "Host" stays the word for the
application on the other side, whether that is a widget container or an
application rendering the component.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Timo K.
2026-09-08 15:39:00 +02:00
co-authored by Claude Fable 5.1
parent f70fba1dd6
commit 3dc7cfe10b
14 changed files with 48 additions and 47 deletions
+2 -2
View File
@@ -17,8 +17,8 @@ they are meant to cover the page — resolve against the viewport and appear in
the middle of the host's window rather than in the middle of the call. the middle of the host's window rather than in the middle of the call.
The clipping cuts both ways: a menu near the edge of a small container is The clipping cuts both ways: a menu near the edge of a small container is
trimmed rather than overflowing into the host. That is the trade being an trimmed rather than overflowing into the host. That is the trade being a
embedded component makes. */ component rather than a page makes. */
.root { .root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+1 -1
View File
@@ -41,7 +41,7 @@ const IS_ROOT = `:where(${ROOT_SELECTOR})`;
* *
* As a page of its own, Element Call can style the document: normalize.css and * As a page of its own, Element Call can style the document: normalize.css and
* Compound speak of `html`, `body` and bare elements, and the design tokens are * Compound speak of `html`, `body` and bare elements, and the design tokens are
* declared on `:root`. Embedded in a host, all of that would land on the host's * declared on `:root`. As a component, all of that would land on the host's
* document too. This rewrites every selector so that it matches only the root * document too. This rewrites every selector so that it matches only the root
* or its descendants: * or its descendants:
* *
+7 -7
View File
@@ -83,7 +83,7 @@ interface LogEntry {
} }
/** /**
* One embedded Element Call, with the controls a host would have over it: the * One Element Call component, with the controls a host would have over it: the
* requests it can make of Element Call, and the ability to take it off screen * requests it can make of Element Call, and the ability to take it off screen
* altogether. * altogether.
*/ */
@@ -198,9 +198,9 @@ const HostChrome: FC = (): ReactNode => (
); );
/** /**
* A dialog of the host's own, over the top of the calls. Element Call embedded * A dialog of the host's own, over the top of the calls. Element Call as a
* in a host has to sit underneath this — being unable to is one of the reasons * component has to sit underneath this — being unable to is one of the reasons
* for embedding it rather than putting it in an iframe. * for a component rather than an iframe.
*/ */
const HostDialog: FC<{ onClose: () => void }> = ({ onClose }): ReactNode => ( const HostDialog: FC<{ onClose: () => void }> = ({ onClose }): ReactNode => (
<div className={styles.dialogScrim}> <div className={styles.dialogScrim}>
@@ -213,7 +213,7 @@ const HostDialog: FC<{ onClose: () => void }> = ({ onClose }): ReactNode => (
); );
/** /**
* Stands in for a host application embedding Element Call: it owns the Matrix * Stands in for a host application using the Element Call component: it owns the Matrix
* clients, the page and the space each call is given, and reaches Element Call * clients, the page and the space each call is given, and reaches Element Call
* only through the component's public interface. * only through the component's public interface.
* *
@@ -299,8 +299,8 @@ export const Harness: FC = (): ReactNode => {
<form className={styles.credentials} onSubmit={start}> <form className={styles.credentials} onSubmit={start}>
<h1>Element Call component harness</h1> <h1>Element Call component harness</h1>
<p> <p>
Signs in twice and shows Element Call embedded twice, in a page that Signs in twice and shows the Element Call component twice, in a page
is not Element Call&apos;s own. that is not Element Call&apos;s own.
</p> </p>
{field("homeserver", "Homeserver")} {field("homeserver", "Homeserver")}
{field("username", "Username")} {field("username", "Username")}
+2 -2
View File
@@ -31,8 +31,8 @@ import { useInitial } from "../src/useInitial";
import { useLatest } from "../src/useLatest"; import { useLatest } from "../src/useLatest";
/** /**
* What Element Call tells the application embedding it. Everything is * What Element Call tells the application hosting it as a component.
* optional: a host implements what it wants to hear about. * Everything is optional: a host implements what it wants to hear about.
* *
* Compared by nothing — Element Call always calls whichever one it was most * Compared by nothing — Element Call always calls whichever one it was most
* recently given, so this may be written inline. * recently given, so this may be written inline.
+4 -4
View File
@@ -58,7 +58,7 @@ import { HostBridgeProvider } from "../src/HostBridge";
import { RootElementProvider } from "../src/RootElementContext"; import { RootElementProvider } from "../src/RootElementContext";
import { import {
configurationForIntent, configurationForIntent,
hostedProperties, componentProperties,
type UrlParams, type UrlParams,
UrlParamsProvider, UrlParamsProvider,
UserIntent, UserIntent,
@@ -209,7 +209,7 @@ export const ElementCall: FC<ElementCallProps> = ({
const stableConfig = useStableValue(config); const stableConfig = useStableValue(config);
const params = useMemo( const params = useMemo(
(): UrlParams => ({ (): UrlParams => ({
...hostedProperties, ...componentProperties,
roomId, roomId,
...configurationForIntent(intent), ...configurationForIntent(intent),
...stableConfig, ...stableConfig,
@@ -249,8 +249,8 @@ export const ElementCall: FC<ElementCallProps> = ({
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<HostBridgeProvider value={hostBridge}> <HostBridgeProvider value={hostBridge}>
<UrlParamsProvider value={params}> <UrlParamsProvider value={params}>
{/* Element Call's own navigation stays in memory, so that being {/* Element Call's own navigation stays in memory, so that the
embedded cannot disturb the host's URL. */} component cannot disturb the host's URL. */}
<MemoryRouter> <MemoryRouter>
<div ref={setContainer} className={styles.root}> <div ref={setContainer} className={styles.root}>
{container !== null && {container !== null &&
+3 -3
View File
@@ -136,9 +136,9 @@ interface Props {
/** /**
* The client Element Call should use. * The client Element Call should use.
* *
* When a host embeds Element Call it already has a client, and owns the * An application hosting Element Call as a component already has a client,
* user's session; supplying it here means Element Call neither authenticates * and owns the user's session; supplying it here means Element Call neither
* anyone nor manages their session. Left out, Element Call finds a client * authenticates anyone nor manages their session. Left out, Element Call finds a client
* itself — from the widget API, or by restoring or creating a session of its * itself — from the widget API, or by restoring or creating a session of its
* own. * own.
*/ */
+3 -3
View File
@@ -51,7 +51,7 @@ export interface HostRequest<Data, Reply = void> {
* Element Call's view of the application hosting it. * Element Call's view of the application hosting it.
* *
* Element Call can run as its own page, as a widget inside a Matrix client, or * Element Call can run as its own page, as a widget inside a Matrix client, or
* embedded directly into one. Only the last two give it a host, and each of * as a component inside one. Only the last two give it a host, and each of
* them reaches it by a different route — so everything Element Call needs from * them reaches it by a different route — so everything Element Call needs from
* whatever is hosting it goes through this interface, rather than being * whatever is hosting it goes through this interface, rather than being
* expressed in terms of the widget API. * expressed in terms of the widget API.
@@ -101,8 +101,8 @@ export interface HostBridge {
/** /**
* Whether Element Call may offer to change the user's profile — their * Whether Element Call may offer to change the user's profile — their
* display name and avatar. Only when the account is Element Call's own, * display name and avatar. Only when the account is Element Call's own,
* which is to say standalone: a widget's host and an application embedding * which is to say standalone: a widget's host and an application hosting
* Element Call both signed the user in themselves, so the profile is theirs * the component both signed the user in themselves, so the profile is theirs
* to manage and Element Call must not offer to edit it. * to manage and Element Call must not offer to edit it.
*/ */
readonly supportsProfileChanges: boolean; readonly supportsProfileChanges: boolean;
+3 -3
View File
@@ -12,9 +12,9 @@ import { createContext, use } from "react";
* *
* Element Call decorates this element with the theme, layout and background * Element Call decorates this element with the theme, layout and background
* attributes its stylesheets key off, and portals its modals into it. When * attributes its stylesheets key off, and portals its modals into it. When
* Element Call owns the page this is simply the document body; the intent is * Element Call owns the page this is simply the document body; as a component
* that when embedded in a host application it becomes the container the host * it is the container the host mounted it into, so that Element Call does not
* mounted it into, so that Element Call does not reach outside its own subtree. * reach outside its own subtree.
* *
* The stylesheets find this element by its `data-element-call-root` attribute, * The stylesheets find this element by its `data-element-call-root` attribute,
* which {@link useTheme} sets along with the platform and theme, so they no * which {@link useTheme} sets along with the platform and theme, so they no
+2 -2
View File
@@ -17,7 +17,7 @@ import {
computeUrlParams, computeUrlParams,
HeaderStyle, HeaderStyle,
getUrlParams, getUrlParams,
hostedProperties, componentProperties,
UserIntent, UserIntent,
} from "../src/UrlParams"; } from "../src/UrlParams";
import { mockConfig } from "./utils/test"; import { mockConfig } from "./utils/test";
@@ -433,7 +433,7 @@ describe("UrlParams", () => {
// has no URL of its own for any of this to come from // has no URL of its own for any of this to come from
describe("hosted defaults", () => { describe("hosted defaults", () => {
it("assume nothing about a session or a page", () => { it("assume nothing about a session or a page", () => {
expect(hostedProperties).toMatchObject({ expect(componentProperties).toMatchObject({
// The host is not a widget host, and supplies the client itself, so // The host is not a widget host, and supplies the client itself, so
// none of the widget or session plumbing applies // none of the widget or session plumbing applies
isWidget: false, isWidget: false,
+8 -6
View File
@@ -446,7 +446,8 @@ export function configurationForIntent(intent: UserIntent): UrlConfiguration {
} }
/** /**
* The {@link UrlProperties} for Element Call embedded in a host application. * The {@link UrlProperties} for Element Call running as a component inside a
* host application.
* *
* It has no URL of its own to read these from, and it does not need most of * It has no URL of its own to read these from, and it does not need most of
* them: the widget plumbing does not apply, the Matrix client and the analytics * them: the widget plumbing does not apply, the Matrix client and the analytics
@@ -454,7 +455,7 @@ export function configurationForIntent(intent: UserIntent): UrlConfiguration {
* the host's to state through the component's props or Element Call's own * the host's to state through the component's props or Element Call's own
* default. * default.
*/ */
export const hostedProperties: UrlProperties = { export const componentProperties: UrlProperties = {
widgetId: null, widgetId: null,
parentUrl: null, parentUrl: null,
isWidget: false, isWidget: false,
@@ -594,8 +595,9 @@ const UrlParamsContext = createContext<UrlParams | null>(null);
/** /**
* Supplies the parameters Element Call should run with. * Supplies the parameters Element Call should run with.
* *
* The standalone and widget builds derive these from the URL, but an embedder * The standalone and widget builds derive these from the URL, but the
* has no URL of its own to put them in, so it provides them directly instead. * component has no URL of its own to read them from, so its host provides them
* directly instead.
* *
* TODO: `UrlParams` is no longer an accurate name now that these need not come * TODO: `UrlParams` is no longer an accurate name now that these need not come
* from a URL. Renaming it touches every consumer, so it is left until the rest * from a URL. Renaming it touches every consumer, so it is left until the rest
@@ -615,8 +617,8 @@ export const useUrlParams = (): UrlParams =>
/** /**
* Derives {@link UrlParams} from the current router location. * Derives {@link UrlParams} from the current router location.
* *
* Only meaningful when Element Call owns the URL; embedders provide the params * Only meaningful when Element Call owns the URL; the component is given its
* directly through {@link UrlParamsProvider}. * params directly through {@link UrlParamsProvider}.
*/ */
export const useUrlParamsFromLocation = (): UrlParams => { export const useUrlParamsFromLocation = (): UrlParams => {
const { search, hash } = useLocation(); const { search, hash } = useLocation();
+3 -3
View File
@@ -8,9 +8,9 @@ Please see LICENSE in the repository root for full details.
/* The styles Element Call needs wherever it is shown: the design tokens, fonts /* The styles Element Call needs wherever it is shown: the design tokens, fonts
and element defaults its own stylesheets build on top of. and element defaults its own stylesheets build on top of.
Split out from index.css so that Element Call embedded in a host application Split out from index.css so that Element Call as a component can have these
can have these without also being given the standalone page's layout, which without also being given the standalone page's layout, which would style the
would style the host's own document. What remains here still speaks of the host's own document. What remains here still speaks of the
document — normalize.css and the typography below use bare element selectors, document — normalize.css and the typography below use bare element selectors,
and the custom properties are declared on `:root` — which is right for the and the custom properties are declared on `:root` — which is right for the
page, and is why the component build rewrites it: there every selector is page, and is why the component build rewrites it: there every selector is
+5 -6
View File
@@ -36,7 +36,7 @@ export class Config {
* *
* Does nothing if the config has already been initialized, including by * Does nothing if the config has already been initialized, including by
* {@link Config.initWith}, so that the regular startup path can run unchanged * {@link Config.initWith}, so that the regular startup path can run unchanged
* when an embedder has already supplied the config. * when a component host has already supplied the config.
*/ */
public static async init(): Promise<void> { public static async init(): Promise<void> {
if (!Config.internalInstance?.initPromise) { if (!Config.internalInstance?.initPromise) {
@@ -66,16 +66,15 @@ export class Config {
} }
/** /**
* Initializes the config from an object supplied by the embedder, instead of * Initializes the config from an object supplied by the application hosting
* fetching `config.json`. * the component, instead of fetching `config.json`.
* *
* {@link Config.init} derives the location of `config.json` from * {@link Config.init} derives the location of `config.json` from
* `window.location`, which only makes sense while Element Call owns the page. * `window.location`, which only makes sense while Element Call owns the page.
* When it is embedded in a host application the host owns the configuration * As a component, the host owns the configuration and passes it in here.
* and passes it in here.
* *
* The config goes through the same validation and defaulting as a fetched * The config goes through the same validation and defaulting as a fetched
* one, so that an injected config behaves identically to a hosted one. * one, so that a supplied config behaves identically to a fetched one.
* *
* Replaces any config initialized earlier. * Replaces any config initialized earlier.
*/ */
+1 -1
View File
@@ -229,7 +229,7 @@ export interface CallViewModelOptions {
* *
* Callers share this rather than picking the fields out themselves. The * Callers share this rather than picking the fields out themselves. The
* defaults on {@link CallViewModelOptions} describe a standalone Element Call, * defaults on {@link CallViewModelOptions} describe a standalone Element Call,
* so a widget or embedded caller that misses one does not get an error — it * so a widget or component caller that misses one does not get an error — it
* quietly gets standalone behaviour instead. * quietly gets standalone behaviour instead.
* *
* Note `autoLeaveWhenOthersLeft` and `waitForCallPickup` are deliberately not * Note `autoLeaveWhenOthersLeft` and `waitForCallPickup` are deliberately not
+4 -4
View File
@@ -13,10 +13,10 @@ export const i18nKey = (key: string): string => key;
/** /**
* Element Call's own i18next instance. * Element Call's own i18next instance.
* *
* We deliberately do not use the global i18next singleton: when Element Call is * We deliberately do not use the global i18next singleton: when Element Call
* embedded in a host application (rather than running as its own page), that * runs as a component inside a host application (rather than as its own page),
* singleton belongs to the host, and configuring it would clobber the host's * that singleton belongs to the host, and configuring it would clobber the
* translations. * host's translations.
* *
* It is configured by `Initializer.initBeforeReact` and made available to * It is configured by `Initializer.initBeforeReact` and made available to
* components via `<I18nextProvider>`; tests and stories configure it directly. * components via `<I18nextProvider>`; tests and stories configure it directly.