merge: upstream Element Call v0.28.0 (cinny #201) #61
+4
-4
@@ -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
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user