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 61 additions and 19 deletions
Showing only changes of commit 0dca5835f6 - Show all commits
+4 -4
View File
@@ -34,6 +34,7 @@ import { MediaDevicesContext } from "./MediaDevicesContext";
import { getUrlParams, HeaderStyle, useUrlParams } from "./UrlParams";
import { AppBar } from "./AppBar";
import { i18n } from "./utils/i18n";
import { useRootElement } from "./RootElementContext";
const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route);
@@ -44,12 +45,11 @@ interface SimpleProviderProps {
const BackgroundProvider: FC<SimpleProviderProps> = ({ children }) => {
const { pathname } = useLocation();
const { background } = useUrlParams();
const rootElement = useRootElement();
useEffect(() => {
document
.getElementsByTagName("body")[0]
.setAttribute("data-background", background);
}, [pathname, background]);
rootElement.setAttribute("data-background", background);
}, [pathname, background, rootElement]);
return children;
};
+4 -2
View File
@@ -24,6 +24,7 @@ import { Heading, Glass } from "@vector-im/compound-web";
import styles from "./Modal.module.css";
import overlayStyles from "./Overlay.module.css";
import { useMediaQuery } from "./useMediaQuery";
import { useRootElement } from "./RootElementContext";
export interface Props {
title: string;
@@ -78,6 +79,7 @@ export const Modal: FC<Props> = ({
...rest
}) => {
const { t } = useTranslation();
const rootElement = useRootElement();
// Empirically, Chrome on Android can end up not matching (hover: none), but
// still matching (pointer: coarse) :/
const touchscreen = useMediaQuery("(hover: none) or (pointer: coarse)");
@@ -100,7 +102,7 @@ export const Modal: FC<Props> = ({
onOpenChange={onOpenChange}
dismissible={onDismiss !== undefined}
>
<Drawer.Portal>
<Drawer.Portal container={rootElement}>
<Drawer.Overlay className={classNames(overlayStyles.bg)} />
<Drawer.Content
className={classNames(
@@ -155,7 +157,7 @@ export const Modal: FC<Props> = ({
return (
<DialogRoot open={open} onOpenChange={onOpenChange}>
<DialogPortal>
<DialogPortal container={rootElement}>
<DialogOverlay
className={classNames(overlayStyles.bg, overlayStyles.animate)}
/>
+30
View File
@@ -0,0 +1,30 @@
/*
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 { createContext, use } from "react";
/**
* The element that Element Call treats as the root of its own interface.
*
* Element Call decorates this element with the theme, layout and background
* attributes its stylesheets key off, and portals its modals into it. When
* Element Call owns the page this is simply the document body; when it is
* embedded in a host application it is the container the host mounted it into,
* so that Element Call does not reach outside its own subtree.
*/
const RootElementContext = createContext<HTMLElement | null>(null);
export const RootElementProvider = RootElementContext.Provider;
/**
* The element Element Call should decorate and portal into.
*
* Defaults to the document body, so that the standalone and widget builds work
* without a provider.
*/
export const useRootElement = (): HTMLElement =>
use(RootElementContext) ?? document.body;
+7 -1
View File
@@ -25,6 +25,7 @@ import { Text } from "@vector-im/compound-web";
import styles from "./Toast.module.css";
import overlayStyles from "./Overlay.module.css";
import { useRootElement } from "./RootElementContext";
interface Props {
/**
@@ -64,6 +65,7 @@ export const Toast: FC<Props> = ({
Icon,
modal = true,
}) => {
const rootElement = useRootElement();
const onOpenChange = useCallback(
(open: boolean) => {
if (!open) onDismiss();
@@ -104,7 +106,11 @@ export const Toast: FC<Props> = ({
return (
<DialogRoot open={open} onOpenChange={onOpenChange} modal={modal}>
{modal ? <DialogPortal>{content}</DialogPortal> : content}
{modal ? (
<DialogPortal container={rootElement}>{content}</DialogPortal>
) : (
content
)}
</DialogRoot>
);
};
+5 -3
View File
@@ -80,6 +80,7 @@ import { useTypedEventEmitter } from "../useEvents";
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
import { useAppBarTitle } from "../AppBar.tsx";
import { useBehavior } from "../useBehavior.ts";
import { useRootElement } from "../RootElementContext.ts";
/**
* If there already are this many participants in the call, we automatically mute
@@ -123,6 +124,7 @@ export const GroupCallView: FC<Props> = ({
null,
);
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const rootElement = useRootElement();
const muteAllAudio = useBehavior(muteAllAudio$);
const leaveSoundContext = useLatest(
@@ -150,11 +152,11 @@ export const GroupCallView: FC<Props> = ({
// viewport sizes smaller than 122px width. (It is actually this exact number: 122px
// tested on different devices...)
useEffect(() => {
document.body.classList.add("no-scroll-body");
rootElement.classList.add("no-scroll-body");
return (): void => {
document.body.classList.remove("no-scroll-body");
rootElement.classList.remove("no-scroll-body");
};
}, []);
}, [rootElement]);
useEffect(() => {
window.rtcSession = rtcSession;
+4 -4
View File
@@ -54,6 +54,7 @@ import { Slider } from "../Slider";
import { platform } from "../Platform";
import { type RingingMediaViewModel } from "../state/media/RingingMediaViewModel";
import { RingingStatus } from "./RingingStatus";
import { useRootElement } from "../RootElementContext";
interface SpotlightItemBaseProps {
ref?: Ref<HTMLDivElement>;
@@ -414,6 +415,7 @@ export const SpotlightTile: FC<Props> = ({
style,
}) => {
const { t } = useTranslation();
const rootElement = useRootElement();
const [ourRef, root$] = useObservableRef<HTMLDivElement | null>(null);
const ref = useMergedRefs(ourRef, theirRef);
const maximised = useBehavior(vm.maximised$);
@@ -428,24 +430,22 @@ export const SpotlightTile: FC<Props> = ({
const canGoToNext = visibleIndex !== -1 && visibleIndex < media.length - 1;
const isFullscreen = useCallback((): boolean => {
const rootElement = document.body;
if (rootElement && document.fullscreenElement) return true;
return false;
}, []);
}, [rootElement]);
const FullScreenIcon = isFullscreen()
? FullScreenMinimiseIcon
: FullScreenMaximiseIcon;
const onToggleFullscreen = useCallback(() => {
const rootElement = document.body;
if (!rootElement) return;
if (isFullscreen()) {
void document?.exitFullscreen();
} else {
void rootElement.requestFullscreen();
}
}, [isFullscreen]);
}, [isFullscreen, rootElement]);
// To keep track of which item is visible, we need an intersection observer
// hooked up to the root element and the items. Because the items will run
+7 -5
View File
@@ -11,12 +11,14 @@ import { type IThemeChangeActionRequest } from "matrix-widget-api";
import { getUrlParams } from "./UrlParams";
import { widget } from "./widget";
import { useRootElement } from "./RootElementContext";
export const useTheme = (): void => {
const rootElement = useRootElement();
const [requestedTheme, setRequestedTheme] = useState(
() => getUrlParams().theme,
);
const previousTheme = useRef<string | null>(document.body.classList.item(0));
const previousTheme = useRef<string | null>(rootElement.classList.item(0));
useEffect(() => {
if (widget) {
@@ -47,15 +49,15 @@ export const useTheme = (): void => {
: "";
const themeString = "cpd-theme-" + theme + themeHighContrast;
if (themeString !== previousTheme.current) {
document.body.classList.remove(
rootElement.classList.remove(
"cpd-theme-light",
"cpd-theme-dark",
"cpd-theme-light-hc",
"cpd-theme-dark-hc",
);
document.body.classList.add(themeString);
rootElement.classList.add(themeString);
previousTheme.current = themeString;
}
document.body.classList.remove("no-theme");
}, [previousTheme, requestedTheme]);
rootElement.classList.remove("no-theme");
}, [previousTheme, requestedTheme, rootElement]);
};