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
7 changed files with 84 additions and 72 deletions
Showing only changes of commit 182be8676d - Show all commits
+10 -16
View File
@@ -54,11 +54,7 @@ import { MediaDevices } from "../src/state/MediaDevices";
import { E2eeType } from "../src/e2ee/e2eeType";
import { currentAndPrev, TEXT_LK_TOPIC, tryMakeSticky } from "./helper";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import {
ElementWidgetActions,
widget as _widget,
initializeWidget,
} from "../src/widget";
import { initializeWidget } from "../src/widget";
import { type Connection } from "../src/state/CallViewModel/remoteMembers/Connection";
import { createWidgetHostBridge } from "../src/HostBridge";
@@ -110,8 +106,7 @@ export async function createMatrixRTCSdk(
const scope = new ObservableScope();
// widget client
initializeWidget(application, true);
const widget = _widget;
const widget = initializeWidget(application, true);
if (!widget) throw Error("No widget. This webapp can only start as a widget");
const client = await widget.client;
const hostBridge = createWidgetHostBridge(widget);
@@ -294,18 +289,17 @@ export async function createMatrixRTCSdk(
});
await leaveResolver.promise;
logger.info("send Unstick");
await widget.api
await hostBridge
.setAlwaysOnScreen(false)
.catch((e) =>
logger.error(
"Failed to set call widget `alwaysOnScreen` to false",
e,
),
.catch((e: unknown) =>
logger.error("Failed to set `alwaysOnScreen` to false", e),
);
logger.info("send Close");
await widget.api.transport
.send(ElementWidgetActions.Close, {})
.catch((e) => logger.error("Failed to send close action", e));
await hostBridge
.close?.()
.catch((e: unknown) =>
logger.error("Failed to ask the host to close", e),
);
};
// schedule close first and then leave (scope.end)
+38 -21
View File
@@ -10,6 +10,7 @@ import { BrowserRouter, Route, useLocation, Routes } from "react-router-dom";
import * as Sentry from "@sentry/react";
import { TooltipProvider } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/lib/logger";
import { type MatrixClient } from "matrix-js-sdk";
import { I18nextProvider } from "react-i18next";
import { HomePage } from "./home/HomePage";
@@ -19,7 +20,7 @@ import { RoomPage } from "./room/RoomPage";
import { ClientProvider } from "./ClientContext";
import { ErrorPage, LoadingPage } from "./FullScreenView";
import { Initializer } from "./initializer";
import { widget } from "./widget";
import { type WidgetHelpers } from "./widget";
import { useTheme } from "./useTheme";
import { ProcessorProvider } from "./livekit/TrackProcessorContext";
import { type AppViewModel } from "./state/AppViewModel";
@@ -81,9 +82,11 @@ const MaybeAppBar: FC<SimpleProviderProps> = ({ children }) => {
interface Props {
vm: AppViewModel;
/** A point of access to the widget API, if running as a widget. */
widget: WidgetHelpers | null;
}
export const App: FC<Props> = ({ vm }) => {
export const App: FC<Props> = ({ vm, widget }) => {
// The standalone build has no host; the widget build's host is the client it
// is a widget of.
const hostBridge = useInitial(() =>
@@ -100,26 +103,40 @@ export const App: FC<Props> = ({ vm }) => {
.catch(logger.error);
});
const content = loaded ? (
<ClientProvider>
<MediaDevicesContext value={vm.mediaDevices}>
<ProcessorProvider>
<Sentry.ErrorBoundary
fallback={(error) => <ErrorPage error={error} />}
>
<Routes>
<SentryRoute path="/" element={<HomePage />} />
<SentryRoute path="/login" element={<LoginPage />} />
<SentryRoute path="/register" element={<RegisterPage />} />
<SentryRoute path="*" element={<RoomPage />} />
</Routes>
</Sentry.ErrorBoundary>
</ProcessorProvider>
</MediaDevicesContext>
</ClientProvider>
) : (
<LoadingPage />
// As a widget, the client comes from the host over the widget API. Standalone,
// Element Call finds one itself, so there is nothing to wait for here.
const [widgetClient, setWidgetClient] = useState<MatrixClient | undefined>(
undefined,
);
useEffect(() => {
if (widget === null) return;
widget.client
.then(setWidgetClient)
.catch((e) => logger.error("Failed to obtain the host's client", e));
}, [widget]);
const clientReady = widget === null || widgetClient !== undefined;
const content =
loaded && clientReady ? (
<ClientProvider client={widgetClient}>
<MediaDevicesContext value={vm.mediaDevices}>
<ProcessorProvider>
<Sentry.ErrorBoundary
fallback={(error) => <ErrorPage error={error} />}
>
<Routes>
<SentryRoute path="/" element={<HomePage />} />
<SentryRoute path="/login" element={<LoginPage />} />
<SentryRoute path="/register" element={<RegisterPage />} />
<SentryRoute path="*" element={<RoomPage />} />
</Routes>
</Sentry.ErrorBoundary>
</ProcessorProvider>
</MediaDevicesContext>
</ClientProvider>
) : (
<LoadingPage />
);
return (
<I18nextProvider i18n={i18n}>
+15 -17
View File
@@ -22,7 +22,6 @@ import { type ISyncStateData, type SyncState } from "matrix-js-sdk/lib/sync";
import { ClientEvent, type MatrixClient } from "matrix-js-sdk";
import { ErrorPage } from "./FullScreenView";
import { widget } from "./widget";
import { useHostBridge } from "./HostBridge";
import {
PosthogAnalytics,
@@ -163,7 +162,12 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
const initializing = useRef(false);
useEffect(() => {
if (client !== undefined) return;
if (client !== undefined) {
// Nothing to load, but analytics still need to follow the user's choices.
if (PosthogAnalytics.instance.isEnabled())
PosthogAnalytics.instance.startListeningToSettingsChanges();
return;
}
// In case the component is mounted, unmounted, and remounted quickly (as
// React does in strict mode), we need to make sure not to doubly initialize
// the client.
@@ -246,9 +250,9 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
// To protect against multiple sessions writing to the same storage
// simultaneously, we send a broadcast message that shuts down all other
// running instances of the app. Element Call only has storage of its own to
// protect when it created the session itself; given a client, or running as a
// widget, it is mostly stateless.
const ownsSession = client === undefined && widget === null;
// protect when it created the session itself; given a client — by a host, or
// over the widget API — it is mostly stateless.
const ownsSession = client === undefined;
useEffect(() => {
if (ownsSession) loadChannel?.postMessage({});
}, [ownsSession]);
@@ -349,19 +353,13 @@ export type InitResult = {
passwordlessUser: boolean;
};
/**
* Restores or creates a session of Element Call's own. Only reached when no
* client was supplied for it to use.
*/
async function loadClient(): Promise<InitResult | null> {
if (widget) {
// We're inside a widget, so let's engage *matryoshka mode*
logger.log("Using a matryoshka client");
const client = await widget.client;
return {
client,
passwordlessUser: false,
};
} else {
const { initSPA } = await import("./utils/spa");
return initSPA(loadSession, clearSession);
}
const { initSPA } = await import("./utils/spa");
return initSPA(loadSession, clearSession);
}
export interface Session {
+5 -3
View File
@@ -32,7 +32,7 @@ import { Config } from "./config/Config";
import { seedSettingsFromConfig } from "./settings/settings";
import { platform } from "./Platform";
import { isFailure } from "./utils/fetch";
import { initializeWidget } from "./widget";
import { initializeWidget, type WidgetHelpers } from "./widget";
import { enableExtendedLivekitLogs } from "./settings/settings.ts";
import {
type AnalyticsConfig,
@@ -155,8 +155,8 @@ export class Initializer {
return !!Initializer.internalInstance?.isInitialized;
}
public static async initBeforeReact(): Promise<void> {
initializeWidget();
public static async initBeforeReact(): Promise<WidgetHelpers | null> {
const widget = initializeWidget();
const polyfills: Promise<unknown>[] = [];
if (shouldPolyfillSegmenter()) {
@@ -243,6 +243,8 @@ export class Initializer {
});
window.setLKLogLevel = setLKLogLevel;
return widget;
}
public static init(): Promise<void> | null {
+2 -1
View File
@@ -49,7 +49,7 @@ if (fatalError !== null) {
}
Initializer.initBeforeReact()
.then(() => {
.then((widget) => {
const { controlledAudioDevices, callIntent } = getUrlParams();
root.render(
<StrictMode>
@@ -60,6 +60,7 @@ Initializer.initBeforeReact()
callIntent,
})
}
widget={widget}
/>
</StrictMode>,
);
+2 -2
View File
@@ -9,7 +9,7 @@ import { describe, expect, vi, it, beforeEach } from "vitest";
import { createRoomWidgetClient, EventType } from "matrix-js-sdk";
import { getUrlParams } from "./UrlParams";
import { initializeWidget, widget } from "./widget";
import { initializeWidget } from "./widget";
import { Config } from "./config/Config";
import { ElementCallReactionEventType } from "./reactions";
@@ -42,7 +42,7 @@ beforeEach(() => {
describe("widget", () => {
it("should create an embedded client with the correct params", () => {
initializeWidget("ANYRTCAPP");
const widget = initializeWidget("ANYRTCAPP");
expect(getUrlParams()).toStrictEqual({
widgetId: "id",
+12 -12
View File
@@ -58,21 +58,21 @@ export interface WidgetHelpers {
}
/**
* A point of access to the widget API, if the app is running as a widget. This
* is initialized with `initializeWidget`. This should happen at the top level because the widget messaging
* needs to be set up ASAP on load to ensure it doesn't miss any requests.
*/
export let widget: WidgetHelpers | null = null;
/**
* Should be called as soon as possible on app start. (In the initilizer before react)
* Connects to the widget API, if Element Call is running as a widget.
*
* Should be called as soon as possible on app start (in the initializer, before
* React), because the widget messaging needs to be set up ASAP on load to
* ensure it doesn't miss any requests.
*
* @returns A point of access to the widget API, or null if Element Call is not
* running as a widget.
*/
// this needs to be a seperate call and cannot be done on import to allow us to spy on methods in here before
// execution.
export const initializeWidget = (
rtcApplication: string = "m.call",
sendRoomEvents = false,
): void => {
): WidgetHelpers | null => {
try {
const {
widgetId,
@@ -202,14 +202,14 @@ export const initializeWidget = (
return client;
};
widget = { api, lazyActions, client: clientPromise() };
return { api, lazyActions, client: clientPromise() };
} else {
if (import.meta.env.MODE !== "test")
logger.info("No widget API available");
widget = null;
return null;
}
} catch (e) {
logger.warn("Continuing without the widget API", e);
widget = null;
return null;
}
};