mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
The call reached for react-router in five places to send the user
"home": on leaving without a lobby, from the lobby's recents link, from
the post-call screen, from the error page's return button and from the
header logo. Home is the standalone app's home page; the call has no
idea where that is, and a component has no such place at all — its host
decides what follows a call. Yet the component had to mount a
MemoryRouter just so those hooks would not throw.
`useLeaveToHome` is the way home as the shell supplies it: the app
provides `navigate("/")` from inside its router, the component provides
nothing, and everything that used to link to "/" now either calls it or,
when there is none, offers no way out. The logo becomes a plain logo,
the recents and "not now" links disappear, the error page's button does
too. `ClientProvider`'s logout goes the same way. The component no
longer renders a router.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
191 lines
5.9 KiB
TypeScript
191 lines
5.9 KiB
TypeScript
/*
|
|
Copyright 2021-2024 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import {
|
|
type FC,
|
|
type JSX,
|
|
Suspense,
|
|
useCallback,
|
|
useEffect,
|
|
useState,
|
|
} from "react";
|
|
import {
|
|
BrowserRouter,
|
|
Route,
|
|
useLocation,
|
|
useNavigate,
|
|
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";
|
|
import { LoginPage } from "./auth/LoginPage";
|
|
import { RegisterPage } from "./auth/RegisterPage";
|
|
import { RoomPage } from "./room/RoomPage";
|
|
import { ClientProvider } from "./ClientContext";
|
|
import { ErrorPage, LoadingPage } from "./FullScreenView";
|
|
import { Initializer } from "./initializer";
|
|
import { type WidgetHelpers } from "./widget";
|
|
import { useTheme } from "./useTheme";
|
|
import { ProcessorProvider } from "./livekit/TrackProcessorContext";
|
|
import { type AppViewModel } from "./state/AppViewModel";
|
|
import { MediaDevicesContext } from "./MediaDevicesContext";
|
|
import {
|
|
HeaderStyle,
|
|
UrlParamsProvider,
|
|
useUrlParams,
|
|
useUrlParamsFromLocation,
|
|
} from "./UrlParams";
|
|
import { AppBar } from "./AppBar";
|
|
import { i18n } from "./utils/i18n";
|
|
import { useRootElement } from "./RootElementContext";
|
|
import {
|
|
createWidgetHostBridge,
|
|
HostBridgeProvider,
|
|
nullHostBridge,
|
|
} from "./HostBridge";
|
|
import { useInitial } from "./useInitial";
|
|
import { LeaveToHomeProvider } from "./LeaveToHomeContext";
|
|
|
|
const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route);
|
|
|
|
interface SimpleProviderProps {
|
|
children: JSX.Element;
|
|
}
|
|
|
|
/**
|
|
* Supplies the URL-derived params to the rest of the app. Only the standalone
|
|
* and widget builds own the URL, so this lives here in the app shell rather
|
|
* than alongside the context itself.
|
|
*/
|
|
const LocationUrlParamsProvider: FC<SimpleProviderProps> = ({ children }) => {
|
|
const urlParams = useUrlParamsFromLocation();
|
|
return <UrlParamsProvider value={urlParams}>{children}</UrlParamsProvider>;
|
|
};
|
|
|
|
/**
|
|
* Supplies the way home. Only the app has one — its home page, with the list
|
|
* of recent calls — so this, too, lives in the app shell.
|
|
*/
|
|
const HomeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
|
const navigate = useNavigate();
|
|
const leaveToHome = useCallback(() => {
|
|
navigate("/")?.catch((e) => logger.error("Failed to navigate home", e));
|
|
}, [navigate]);
|
|
return (
|
|
<LeaveToHomeProvider value={leaveToHome}>{children}</LeaveToHomeProvider>
|
|
);
|
|
};
|
|
|
|
const BackgroundProvider: FC<SimpleProviderProps> = ({ children }) => {
|
|
const { pathname } = useLocation();
|
|
const { background } = useUrlParams();
|
|
const rootElement = useRootElement();
|
|
|
|
useEffect(() => {
|
|
rootElement.setAttribute("data-background", background);
|
|
}, [pathname, background, rootElement]);
|
|
|
|
return children;
|
|
};
|
|
|
|
const ThemeProvider: FC<SimpleProviderProps> = ({ children }) => {
|
|
useTheme();
|
|
return children;
|
|
};
|
|
|
|
/** Wraps the app in an {@link AppBar}, if the params ask for one. */
|
|
const MaybeAppBar: FC<SimpleProviderProps> = ({ children }) => {
|
|
const { header } = useUrlParams();
|
|
return header === HeaderStyle.AppBar ? <AppBar>{children}</AppBar> : 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, widget }) => {
|
|
// The standalone build has no host; the widget build's host is the client it
|
|
// is a widget of.
|
|
const hostBridge = useInitial(() =>
|
|
widget === null ? nullHostBridge : createWidgetHostBridge(widget),
|
|
);
|
|
const [loaded, setLoaded] = useState(false);
|
|
useEffect(() => {
|
|
Initializer.init()
|
|
?.then(async () => {
|
|
if (loaded) return;
|
|
setLoaded(true);
|
|
await hostBridge.contentLoaded();
|
|
})
|
|
.catch(logger.error);
|
|
});
|
|
|
|
// 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}>
|
|
<HostBridgeProvider value={hostBridge}>
|
|
<BrowserRouter>
|
|
<LocationUrlParamsProvider>
|
|
<HomeProvider>
|
|
<BackgroundProvider>
|
|
<ThemeProvider>
|
|
<TooltipProvider>
|
|
<Suspense fallback={null}>
|
|
<MaybeAppBar>{content}</MaybeAppBar>
|
|
</Suspense>
|
|
</TooltipProvider>
|
|
</ThemeProvider>
|
|
</BackgroundProvider>
|
|
</HomeProvider>
|
|
</LocationUrlParamsProvider>
|
|
</BrowserRouter>
|
|
</HostBridgeProvider>
|
|
</I18nextProvider>
|
|
);
|
|
};
|