merge: upstream Element Call v0.28.0 (cinny #201) #61

Merged
jared merged 197 commits from lotus-upstream-v0.28.0 into lotus 2026-10-24 01:14:32 -04:00
8 changed files with 296 additions and 5 deletions
Showing only changes of commit 979b521563 - Show all commits
+24
View File
@@ -0,0 +1,24 @@
/*
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.
*/
/* The container a host mounts us into. It fills whatever space the host gives
it, and establishes a stacking context of its own so that our overlays and
modals cannot escape it — which is the whole reason for embedding rather than
using an iframe. */
.root {
display: flex;
flex-direction: column;
inline-size: 100%;
block-size: 100%;
isolation: isolate;
position: relative;
background-color: var(--cpd-color-bg-canvas-default);
color: var(--cpd-color-text-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: transparent;
}
+185
View File
@@ -0,0 +1,185 @@
/*
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
*
* Element Call as a React component, for an application that wants to show a
* call inside itself rather than in an iframe.
*
* The host supplies the client and says which room to call in; Element Call
* supplies the call. Everything it would otherwise take from the page it is on
* — the URL, the document body, a Matrix session of its own — comes from the
* host instead, or is confined to the container it is mounted in.
*/
import { type FC, type JSX, type ReactNode, useMemo, useState } from "react";
import { type MatrixClient } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger";
import { MemoryRouter } from "react-router-dom";
import { I18nextProvider } from "react-i18next";
import { TooltipProvider } from "@vector-im/compound-web";
import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill";
import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill.js";
import { ElementCallView } from "../src/ElementCallView";
import { ClientProvider } from "../src/ClientContext";
import {
type HostBridge,
HostBridgeProvider,
nullHostBridge,
} from "../src/HostBridge";
import { RootElementProvider } from "../src/RootElementContext";
import {
computeUrlParams,
type UrlParams,
UrlParamsProvider,
} from "../src/UrlParams";
import { MediaDevicesContext } from "../src/MediaDevicesContext";
import { MediaDevices } from "../src/state/MediaDevices";
import { ObservableScope } from "../src/state/ObservableScope";
import { ProcessorProvider } from "../src/livekit/TrackProcessorContext";
import { Config } from "../src/config/Config";
import { type ConfigOptions } from "../src/config/ConfigOptions";
import { i18n } from "../src/utils/i18n";
import { useTheme } from "../src/useTheme";
import { useInitial } from "../src/useInitial";
import styles from "./ElementCall.module.css";
export { type HostBridge } from "../src/HostBridge";
/**
* How Element Call should behave. Everything is optional; anything left out
* takes the same default it would in the standalone app.
*/
export type ElementCallConfiguration = Partial<UrlParams>;
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;
/** How Element Call should behave. */
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.
*/
hostBridge?: HostBridge;
}
/**
* Prepares the things Element Call needs before it can be shown: translations,
* `Intl` polyfills for older browsers, and its configuration.
*
* Await this once, before rendering {@link ElementCall}.
*/
export async function initializeElementCall(
config: ConfigOptions = {},
): Promise<void> {
const polyfills: Promise<unknown>[] = [];
if (shouldPolyfillSegmenter())
polyfills.push(import("@formatjs/intl-segmenter/polyfill-force"));
if (shouldPolyfillDurationFormat())
polyfills.push(import("@formatjs/intl-durationformat/polyfill-force.js"));
await Promise.all(polyfills);
Config.initWith(config);
await i18n.init({
fallbackLng: "en",
defaultNS: "app",
keySeparator: ".",
nsSeparator: false,
pluralSeparator: "_",
contextSeparator: "|",
lng: "en",
interpolation: { escapeValue: false },
});
}
/** Applies the theme to the container, before it is painted. */
const Decoration: FC<{ children: JSX.Element }> = ({ children }) => {
useTheme();
return children;
};
export const ElementCall: FC<ElementCallProps> = ({
client,
roomId,
config,
hostBridge = nullHostBridge,
}): ReactNode => {
// The container is what Element Call decorates and portals into, so nothing
// inside can render until we have it.
const [container, setContainer] = useState<HTMLDivElement | null>(null);
// The defaults are the standalone app's, with the host's wishes over the top
const params = useMemo(
(): UrlParams => ({ ...computeUrlParams(), ...config }),
[config],
);
const mediaDevices = useInitial(
() =>
new MediaDevices(new ObservableScope(), {
controlledAudioDevices: params.controlledAudioDevices,
callIntent: params.callIntent,
}),
);
const room = client.getRoom(roomId);
const rtcSession = useMemo(
() => (room === null ? null : client.matrixRTC.getRoomSession(room)),
[client, room],
);
if (rtcSession === null)
logger.error(
`Element Call was asked to call in ${roomId}, which its host's client does not know about`,
);
return (
<I18nextProvider i18n={i18n}>
<HostBridgeProvider value={hostBridge}>
<UrlParamsProvider value={params}>
{/* Element Call's own navigation stays in memory, so that being
embedded cannot disturb the host's URL. */}
<MemoryRouter>
<div ref={setContainer} className={styles.root}>
{container !== null && rtcSession !== null && (
<RootElementProvider value={container}>
<Decoration>
<TooltipProvider>
<ClientProvider client={client}>
<MediaDevicesContext value={mediaDevices}>
<ProcessorProvider>
<ElementCallView
client={client}
rtcSession={rtcSession}
isPasswordlessUser={false}
confineToRoom={params.confineToRoom}
preload={params.preload}
skipLobby={params.skipLobby}
/>
</ProcessorProvider>
</MediaDevicesContext>
</ClientProvider>
</TooltipProvider>
</Decoration>
</RootElementProvider>
)}
</div>
</MemoryRouter>
</UrlParamsProvider>
</HostBridgeProvider>
</I18nextProvider>
);
};
+6 -1
View File
@@ -9,7 +9,12 @@ import { type KnipConfig } from "knip";
export default {
vite: {
config: ["vite.config.ts", "vite-embedded.config.ts", "vite-sdk.config.ts"],
config: [
"vite.config.ts",
"vite-embedded.config.ts",
"vite-sdk.config.ts",
"vite-component.config.ts",
],
},
entry: ["src/main.tsx", "eslint/index.js", "i18next.config.ts"],
ignoreBinaries: [
+3
View File
@@ -16,6 +16,9 @@
"build:sdk:development": "pnpm build:sdk --mode development",
"build:sdk": "pnpm build:full --config vite-sdk.config.js",
"build:sdk:production": "pnpm build:sdk",
"build:component": "pnpm build:full --config vite-component.config.js",
"build:component:production": "pnpm build:component",
"build:component:development": "pnpm build:component --mode development",
"serve": "vite preview",
"format": "oxfmt",
"format:check": "oxfmt --check; rc=$?; [[ $rc -ne 0 ]] && printf '\\033[46;30m INFO \\033[0m To fix, run: pnpm format\\n' >&2; exit $rc",
+4
View File
@@ -1,3 +1,7 @@
supportedArchitectures:
os: [current, linux]
cpu: [current, arm64]
libc: [current, glibc]
minimumReleaseAgeExclude:
- "@vector-im/compound-design-tokens"
- "@vector-im/compound-web"
+6 -3
View File
@@ -25,11 +25,14 @@ import { createContext, use } from "react";
* `index.html` starts the body hidden with `no-theme` until the theme lands.
* Neither applies when a host mounts Element Call into a container of its own.
*/
// No provider is exported yet: nothing supplies a root element, so every
// consumer falls back to the document body. One arrives with the entry point
// that mounts Element Call into a container.
const RootElementContext = createContext<HTMLElement | null>(null);
/**
* Supplies the element Element Call should confine itself to. The standalone
* and widget builds need no provider, since for them that element is the body.
*/
export const RootElementProvider = RootElementContext.Provider;
/**
* The element Element Call should decorate and portal into.
*
+3 -1
View File
@@ -53,7 +53,9 @@
"./src/**/*.ts",
"./src/**/*.tsx",
"./playwright/**/*.ts",
"./sdk/**/*.ts"
"./sdk/**/*.ts",
"./component/**/*.ts",
"./component/**/*.tsx"
],
"exclude": ["**.test.ts"]
}
+65
View File
@@ -0,0 +1,65 @@
/*
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 { defineConfig } from "vite";
import { vitePluginsConfig } from "./vite.config";
// Config for Element Call as a React component, to be imported by an
// application embedding it rather than served as a page of its own.
//
// Deliberately not built on top of the full app's config, which exists to
// produce a page and brings an HTML entry point along with it.
export default defineConfig(({ mode }) => ({
...vitePluginsConfig({ mode }),
build: {
minify: mode === "production",
sourcemap: true,
// One stylesheet rather than one per chunk, so a host has a single file to
// include
cssCodeSplit: false,
lib: {
formats: ["es" as const],
entry: "./component/index.tsx",
fileName: "element-call",
},
rollupOptions: {
// The host already has these, and a second copy of any of them does not
// merely bloat the bundle: React would hold two sets of hooks, and the
// Matrix client would run two sync loops.
//
// Every subpath has to be named. Element Call reaches most of the Matrix
// SDK as `matrix-js-sdk/lib/…`, and a bare "matrix-js-sdk" would not
// catch those — while the pattern and callback forms of this option are
// silently ignored by the bundler, so they cannot be used to cover them.
// `pnpm lint:externals` fails if an import appears that is not listed.
external: [
"react",
"react/jsx-runtime",
"react-dom",
"react-dom/client",
"livekit-client",
"matrix-js-sdk",
"matrix-js-sdk/lib/client",
"matrix-js-sdk/lib/crypto-api",
"matrix-js-sdk/lib/logger",
"matrix-js-sdk/lib/matrix",
"matrix-js-sdk/lib/matrixrtc",
"matrix-js-sdk/lib/matrixrtc/EncryptionManager",
"matrix-js-sdk/lib/matrixrtc/IKeyTransport",
"matrix-js-sdk/lib/matrixrtc/IMembershipManager",
"matrix-js-sdk/lib/models/relations-container",
"matrix-js-sdk/lib/models/room",
"matrix-js-sdk/lib/models/typed-event-emitter",
"matrix-js-sdk/lib/randomstring",
"matrix-js-sdk/lib/sync",
"matrix-js-sdk/lib/types",
"matrix-js-sdk/lib/utils",
],
},
},
}));