mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
Return the widget from initializeWidget instead of assigning a global
Element Call reached the widget API through a mutable module-level binding, which every consumer imported directly. Nothing outside the app shell needs it any more, so hand it back from initializeWidget and thread it through: the initializer returns it, main passes it to App, and App uses it to build the host bridge and to await the client the host is lending us. ClientContext's loadClient is now only about restoring or creating a session of Element Call's own, since a widget's client arrives as a prop like any other host's would. Also fixes an early return added in the previous commit, which skipped starting the analytics settings listener when a client was supplied. That was harmless until now, but would have stopped analytics following the user's choices in widget mode. sdk/main.ts asked the host to close by hand; it now uses the bridge, which also stops the transport as the app does.
This commit is contained in:
+10
-16
@@ -54,11 +54,7 @@ import { MediaDevices } from "../src/state/MediaDevices";
|
|||||||
import { E2eeType } from "../src/e2ee/e2eeType";
|
import { E2eeType } from "../src/e2ee/e2eeType";
|
||||||
import { currentAndPrev, TEXT_LK_TOPIC, tryMakeSticky } from "./helper";
|
import { currentAndPrev, TEXT_LK_TOPIC, tryMakeSticky } from "./helper";
|
||||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
import {
|
import { initializeWidget } from "../src/widget";
|
||||||
ElementWidgetActions,
|
|
||||||
widget as _widget,
|
|
||||||
initializeWidget,
|
|
||||||
} from "../src/widget";
|
|
||||||
import { type Connection } from "../src/state/CallViewModel/remoteMembers/Connection";
|
import { type Connection } from "../src/state/CallViewModel/remoteMembers/Connection";
|
||||||
import { createWidgetHostBridge } from "../src/HostBridge";
|
import { createWidgetHostBridge } from "../src/HostBridge";
|
||||||
|
|
||||||
@@ -110,8 +106,7 @@ export async function createMatrixRTCSdk(
|
|||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
|
|
||||||
// widget client
|
// widget client
|
||||||
initializeWidget(application, true);
|
const widget = initializeWidget(application, true);
|
||||||
const widget = _widget;
|
|
||||||
if (!widget) throw Error("No widget. This webapp can only start as a widget");
|
if (!widget) throw Error("No widget. This webapp can only start as a widget");
|
||||||
const client = await widget.client;
|
const client = await widget.client;
|
||||||
const hostBridge = createWidgetHostBridge(widget);
|
const hostBridge = createWidgetHostBridge(widget);
|
||||||
@@ -294,18 +289,17 @@ export async function createMatrixRTCSdk(
|
|||||||
});
|
});
|
||||||
await leaveResolver.promise;
|
await leaveResolver.promise;
|
||||||
logger.info("send Unstick");
|
logger.info("send Unstick");
|
||||||
await widget.api
|
await hostBridge
|
||||||
.setAlwaysOnScreen(false)
|
.setAlwaysOnScreen(false)
|
||||||
.catch((e) =>
|
.catch((e: unknown) =>
|
||||||
logger.error(
|
logger.error("Failed to set `alwaysOnScreen` to false", e),
|
||||||
"Failed to set call widget `alwaysOnScreen` to false",
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
logger.info("send Close");
|
logger.info("send Close");
|
||||||
await widget.api.transport
|
await hostBridge
|
||||||
.send(ElementWidgetActions.Close, {})
|
.close?.()
|
||||||
.catch((e) => logger.error("Failed to send close action", e));
|
.catch((e: unknown) =>
|
||||||
|
logger.error("Failed to ask the host to close", e),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// schedule close first and then leave (scope.end)
|
// schedule close first and then leave (scope.end)
|
||||||
|
|||||||
+38
-21
@@ -10,6 +10,7 @@ import { BrowserRouter, Route, useLocation, Routes } from "react-router-dom";
|
|||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
|
|
||||||
import { HomePage } from "./home/HomePage";
|
import { HomePage } from "./home/HomePage";
|
||||||
@@ -19,7 +20,7 @@ import { RoomPage } from "./room/RoomPage";
|
|||||||
import { ClientProvider } from "./ClientContext";
|
import { ClientProvider } from "./ClientContext";
|
||||||
import { ErrorPage, LoadingPage } from "./FullScreenView";
|
import { ErrorPage, LoadingPage } from "./FullScreenView";
|
||||||
import { Initializer } from "./initializer";
|
import { Initializer } from "./initializer";
|
||||||
import { widget } from "./widget";
|
import { type WidgetHelpers } from "./widget";
|
||||||
import { useTheme } from "./useTheme";
|
import { useTheme } from "./useTheme";
|
||||||
import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
||||||
import { type AppViewModel } from "./state/AppViewModel";
|
import { type AppViewModel } from "./state/AppViewModel";
|
||||||
@@ -81,9 +82,11 @@ const MaybeAppBar: FC<SimpleProviderProps> = ({ children }) => {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
vm: AppViewModel;
|
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
|
// The standalone build has no host; the widget build's host is the client it
|
||||||
// is a widget of.
|
// is a widget of.
|
||||||
const hostBridge = useInitial(() =>
|
const hostBridge = useInitial(() =>
|
||||||
@@ -100,26 +103,40 @@ export const App: FC<Props> = ({ vm }) => {
|
|||||||
.catch(logger.error);
|
.catch(logger.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = loaded ? (
|
// As a widget, the client comes from the host over the widget API. Standalone,
|
||||||
<ClientProvider>
|
// Element Call finds one itself, so there is nothing to wait for here.
|
||||||
<MediaDevicesContext value={vm.mediaDevices}>
|
const [widgetClient, setWidgetClient] = useState<MatrixClient | undefined>(
|
||||||
<ProcessorProvider>
|
undefined,
|
||||||
<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 />
|
|
||||||
);
|
);
|
||||||
|
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 (
|
return (
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
|
|||||||
+15
-17
@@ -22,7 +22,6 @@ import { type ISyncStateData, type SyncState } from "matrix-js-sdk/lib/sync";
|
|||||||
import { ClientEvent, type MatrixClient } from "matrix-js-sdk";
|
import { ClientEvent, type MatrixClient } from "matrix-js-sdk";
|
||||||
|
|
||||||
import { ErrorPage } from "./FullScreenView";
|
import { ErrorPage } from "./FullScreenView";
|
||||||
import { widget } from "./widget";
|
|
||||||
import { useHostBridge } from "./HostBridge";
|
import { useHostBridge } from "./HostBridge";
|
||||||
import {
|
import {
|
||||||
PosthogAnalytics,
|
PosthogAnalytics,
|
||||||
@@ -163,7 +162,12 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
|
|||||||
|
|
||||||
const initializing = useRef(false);
|
const initializing = useRef(false);
|
||||||
useEffect(() => {
|
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
|
// 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
|
// React does in strict mode), we need to make sure not to doubly initialize
|
||||||
// the client.
|
// the client.
|
||||||
@@ -246,9 +250,9 @@ export const ClientProvider: FC<Props> = ({ children, client }) => {
|
|||||||
// To protect against multiple sessions writing to the same storage
|
// To protect against multiple sessions writing to the same storage
|
||||||
// simultaneously, we send a broadcast message that shuts down all other
|
// 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
|
// 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
|
// protect when it created the session itself; given a client — by a host, or
|
||||||
// widget, it is mostly stateless.
|
// over the widget API — it is mostly stateless.
|
||||||
const ownsSession = client === undefined && widget === null;
|
const ownsSession = client === undefined;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ownsSession) loadChannel?.postMessage({});
|
if (ownsSession) loadChannel?.postMessage({});
|
||||||
}, [ownsSession]);
|
}, [ownsSession]);
|
||||||
@@ -349,19 +353,13 @@ export type InitResult = {
|
|||||||
passwordlessUser: boolean;
|
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> {
|
async function loadClient(): Promise<InitResult | null> {
|
||||||
if (widget) {
|
const { initSPA } = await import("./utils/spa");
|
||||||
// We're inside a widget, so let's engage *matryoshka mode*
|
return initSPA(loadSession, clearSession);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
|
|||||||
+5
-3
@@ -32,7 +32,7 @@ import { Config } from "./config/Config";
|
|||||||
import { seedSettingsFromConfig } from "./settings/settings";
|
import { seedSettingsFromConfig } from "./settings/settings";
|
||||||
import { platform } from "./Platform";
|
import { platform } from "./Platform";
|
||||||
import { isFailure } from "./utils/fetch";
|
import { isFailure } from "./utils/fetch";
|
||||||
import { initializeWidget } from "./widget";
|
import { initializeWidget, type WidgetHelpers } from "./widget";
|
||||||
import { enableExtendedLivekitLogs } from "./settings/settings.ts";
|
import { enableExtendedLivekitLogs } from "./settings/settings.ts";
|
||||||
import {
|
import {
|
||||||
type AnalyticsConfig,
|
type AnalyticsConfig,
|
||||||
@@ -155,8 +155,8 @@ export class Initializer {
|
|||||||
return !!Initializer.internalInstance?.isInitialized;
|
return !!Initializer.internalInstance?.isInitialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async initBeforeReact(): Promise<void> {
|
public static async initBeforeReact(): Promise<WidgetHelpers | null> {
|
||||||
initializeWidget();
|
const widget = initializeWidget();
|
||||||
|
|
||||||
const polyfills: Promise<unknown>[] = [];
|
const polyfills: Promise<unknown>[] = [];
|
||||||
if (shouldPolyfillSegmenter()) {
|
if (shouldPolyfillSegmenter()) {
|
||||||
@@ -243,6 +243,8 @@ export class Initializer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.setLKLogLevel = setLKLogLevel;
|
window.setLKLogLevel = setLKLogLevel;
|
||||||
|
|
||||||
|
return widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static init(): Promise<void> | null {
|
public static init(): Promise<void> | null {
|
||||||
|
|||||||
+2
-1
@@ -49,7 +49,7 @@ if (fatalError !== null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Initializer.initBeforeReact()
|
Initializer.initBeforeReact()
|
||||||
.then(() => {
|
.then((widget) => {
|
||||||
const { controlledAudioDevices, callIntent } = getUrlParams();
|
const { controlledAudioDevices, callIntent } = getUrlParams();
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
@@ -60,6 +60,7 @@ Initializer.initBeforeReact()
|
|||||||
callIntent,
|
callIntent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
widget={widget}
|
||||||
/>
|
/>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import { describe, expect, vi, it, beforeEach } from "vitest";
|
|||||||
import { createRoomWidgetClient, EventType } from "matrix-js-sdk";
|
import { createRoomWidgetClient, EventType } from "matrix-js-sdk";
|
||||||
|
|
||||||
import { getUrlParams } from "./UrlParams";
|
import { getUrlParams } from "./UrlParams";
|
||||||
import { initializeWidget, widget } from "./widget";
|
import { initializeWidget } from "./widget";
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
import { ElementCallReactionEventType } from "./reactions";
|
import { ElementCallReactionEventType } from "./reactions";
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe("widget", () => {
|
describe("widget", () => {
|
||||||
it("should create an embedded client with the correct params", () => {
|
it("should create an embedded client with the correct params", () => {
|
||||||
initializeWidget("ANYRTCAPP");
|
const widget = initializeWidget("ANYRTCAPP");
|
||||||
|
|
||||||
expect(getUrlParams()).toStrictEqual({
|
expect(getUrlParams()).toStrictEqual({
|
||||||
widgetId: "id",
|
widgetId: "id",
|
||||||
|
|||||||
+12
-12
@@ -58,21 +58,21 @@ export interface WidgetHelpers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A point of access to the widget API, if the app is running as a widget. This
|
* Connects to the widget API, if Element Call is running as a widget.
|
||||||
* 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.
|
* 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
|
||||||
export let widget: WidgetHelpers | null = null;
|
* ensure it doesn't miss any requests.
|
||||||
|
*
|
||||||
/**
|
* @returns A point of access to the widget API, or null if Element Call is not
|
||||||
* Should be called as soon as possible on app start. (In the initilizer before react)
|
* 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
|
// this needs to be a seperate call and cannot be done on import to allow us to spy on methods in here before
|
||||||
// execution.
|
// execution.
|
||||||
export const initializeWidget = (
|
export const initializeWidget = (
|
||||||
rtcApplication: string = "m.call",
|
rtcApplication: string = "m.call",
|
||||||
sendRoomEvents = false,
|
sendRoomEvents = false,
|
||||||
): void => {
|
): WidgetHelpers | null => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
widgetId,
|
widgetId,
|
||||||
@@ -202,14 +202,14 @@ export const initializeWidget = (
|
|||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|
||||||
widget = { api, lazyActions, client: clientPromise() };
|
return { api, lazyActions, client: clientPromise() };
|
||||||
} else {
|
} else {
|
||||||
if (import.meta.env.MODE !== "test")
|
if (import.meta.env.MODE !== "test")
|
||||||
logger.info("No widget API available");
|
logger.info("No widget API available");
|
||||||
widget = null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn("Continuing without the widget API", e);
|
logger.warn("Continuing without the widget API", e);
|
||||||
widget = null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user