Merge branch 'livekit' into remove-walkie-talkie

This commit is contained in:
Robin
2023-09-18 11:52:52 -04:00
27 changed files with 1494 additions and 1557 deletions

View File

@@ -29,8 +29,9 @@
"@opentelemetry/instrumentation-document-load": "^0.33.0", "@opentelemetry/instrumentation-document-load": "^0.33.0",
"@opentelemetry/instrumentation-user-interaction": "^0.33.0", "@opentelemetry/instrumentation-user-interaction": "^0.33.0",
"@opentelemetry/sdk-trace-web": "^1.9.1", "@opentelemetry/sdk-trace-web": "^1.9.1",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-visually-hidden": "^1.0.3",
"@react-aria/button": "^3.3.4", "@react-aria/button": "^3.3.4",
"@react-aria/dialog": "^3.1.4",
"@react-aria/focus": "^3.5.0", "@react-aria/focus": "^3.5.0",
"@react-aria/menu": "^3.3.0", "@react-aria/menu": "^3.3.0",
"@react-aria/overlays": "^3.7.3", "@react-aria/overlays": "^3.7.3",
@@ -40,7 +41,6 @@
"@react-aria/utils": "^3.10.0", "@react-aria/utils": "^3.10.0",
"@react-spring/web": "^9.4.4", "@react-spring/web": "^9.4.4",
"@react-stately/collections": "^3.3.4", "@react-stately/collections": "^3.3.4",
"@react-stately/overlays": "^3.1.3",
"@react-stately/select": "^3.1.3", "@react-stately/select": "^3.1.3",
"@react-stately/tooltip": "^3.0.5", "@react-stately/tooltip": "^3.0.5",
"@react-stately/tree": "^3.2.0", "@react-stately/tree": "^3.2.0",
@@ -52,7 +52,6 @@
"@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-react": "^4.0.1", "@vitejs/plugin-react": "^4.0.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"color-hash": "^2.0.1",
"events": "^3.3.0", "events": "^3.3.0",
"i18next": "^21.10.0", "i18next": "^21.10.0",
"i18next-browser-languagedetector": "^6.1.8", "i18next-browser-languagedetector": "^6.1.8",
@@ -77,11 +76,13 @@
"sdp-transform": "^2.14.1", "sdp-transform": "^2.14.1",
"tinyqueue": "^2.0.3", "tinyqueue": "^2.0.3",
"unique-names-generator": "^4.6.0", "unique-names-generator": "^4.6.0",
"uuid": "9" "uuid": "9",
"vaul": "^0.6.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.16.5", "@babel/core": "^7.16.5",
"@react-spring/rafz": "^9.7.3", "@react-spring/rafz": "^9.7.3",
"@react-types/dialog": "^3.5.5",
"@sentry/vite-plugin": "^0.3.0", "@sentry/vite-plugin": "^0.3.0",
"@storybook/react": "^6.5.0-alpha.5", "@storybook/react": "^6.5.0-alpha.5",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",

31
src/Glass.module.css Normal file
View File

@@ -0,0 +1,31 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.glass {
border-radius: 36px;
padding: 11px;
border: 1px solid var(--cpd-color-alpha-gray-400);
background: var(--cpd-color-alpha-gray-400);
backdrop-filter: blur(10px);
}
.glass > * {
border-radius: 24px;
}
.glass.frosted {
backdrop-filter: blur(20px);
}

52
src/Glass.tsx Normal file
View File

@@ -0,0 +1,52 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
ComponentPropsWithoutRef,
ReactNode,
forwardRef,
Children,
} from "react";
import classNames from "classnames";
import styles from "./Glass.module.css";
interface Props extends ComponentPropsWithoutRef<"div"> {
children: ReactNode;
className?: string;
/**
* Increases the blur effect.
* @default false
*/
frosted?: boolean;
}
/**
* Adds a border of glass around a child component.
*/
export const Glass = forwardRef<HTMLDivElement, Props>(
({ frosted = false, children, className, ...rest }, ref) => (
<div
ref={ref}
className={classNames(className, styles.glass, {
[styles.frosted]: frosted,
})}
{...rest}
>
{Children.only(children)}
</div>
)
);

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2022 New Vector Ltd Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -14,77 +14,204 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.modalOverlay { .overlay {
position: fixed; position: fixed;
z-index: 100; z-index: 100;
top: 0; inset: 0;
left: 0; background: rgba(3, 12, 27, 0.528);
bottom: 0; }
right: 0;
background: rgba(23, 25, 28, 0.5); @keyframes fade-in {
display: flex; from {
align-items: center; opacity: 0;
justify-content: center; }
to {
opacity: 1;
}
}
.dialogOverlay[data-state="open"] {
animation: fade-in 200ms;
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.dialogOverlay[data-state="closed"] {
animation: fade-out 130ms;
} }
.modal { .modal {
background: var(--cpd-color-bg-subtle-secondary); position: fixed;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15); z-index: 101;
border-radius: 8px;
max-width: 90vw;
width: 600px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.modalHeader { .dialog {
display: flex; left: 50%;
justify-content: space-between; top: 50%;
padding: 34px 32px 0 32px; transform: translate(-50%, -50%);
box-sizing: border-box;
inline-size: 520px;
max-inline-size: 90%;
max-block-size: 600px;
} }
.modalHeader h3 { @keyframes zoom-in {
font-weight: 600; from {
font-size: var(--font-size-title); opacity: 0;
margin: 0; transform: translate(-50%, -50%) scale(80%);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(100%);
}
} }
.closeButton { @keyframes zoom-out {
position: relative; from {
display: flex; opacity: 1;
justify-content: center; transform: translate(-50%, -50%) scale(100%);
align-items: center; }
background-color: transparent; to {
padding: 0; opacity: 0;
border: none; transform: translate(-50%, -50%) scale(80%);
cursor: pointer; }
}
.dialog[data-state="open"] {
animation: zoom-in 200ms;
}
.dialog[data-state="closed"] {
animation: zoom-out 130ms;
}
@media (prefers-reduced-motion) {
.dialog[data-state="open"] {
animation-name: fade-in;
}
.dialog[data-state="closed"] {
animation-name: fade-out;
}
} }
.content { .content {
padding: 24px 32px; display: flex;
flex-direction: column;
overflow: hidden;
} }
.content p { .dialog .content {
margin-top: 0; flex-grow: 1;
background: var(--cpd-color-bg-canvas-default);
} }
@media (max-width: 799px) { .drawer .content {
.modalHeader { overflow: auto;
display: flex; }
justify-content: space-between;
padding: 32px 20px 0 20px;
}
.modal.mobileFullScreen { .drawer {
position: fixed; background: var(--cpd-color-bg-canvas-default);
left: 0; inset-block-end: 0;
right: 0; inset-inline: max(0px, calc((100% - 520px) / 2));
top: 0; max-block-size: 90%;
bottom: 0; border-start-start-radius: 20px;
width: 100%; border-start-end-radius: 20px;
height: 100%; /* Drawer handle comes in the Android style by default */
max-width: none; --handle-block-size: 4px;
max-height: none; --handle-inline-size: 32px;
border-radius: 0; --handle-inset-block-start: var(--cpd-space-4x);
--handle-inset-block-end: var(--cpd-space-4x);
}
body[data-platform="ios"] .drawer {
--handle-block-size: 5px;
--handle-inline-size: 36px;
--handle-inset-block-start: var(--cpd-space-1-5x);
--handle-inset-block-end: calc(var(--cpd-space-1x) / 4);
}
.close {
cursor: pointer;
color: var(--cpd-color-icon-secondary);
border-radius: var(--cpd-radius-pill-effect);
padding: var(--cpd-space-1x);
background: var(--cpd-color-bg-subtle-secondary);
border: none;
}
.close svg {
display: block;
}
@media (hover: hover) {
.close:hover {
background: var(--cpd-color-bg-subtle-primary);
color: var(--cpd-color-icon-primary);
} }
} }
.close:active {
background: var(--cpd-color-bg-subtle-primary);
color: var(--cpd-color-icon-primary);
}
.header {
background: var(--cpd-color-bg-subtle-secondary);
display: grid;
}
.dialog .header {
padding-block-start: var(--cpd-space-4x);
grid-template-columns:
var(--cpd-space-10x) 1fr minmax(var(--cpd-space-6x), auto)
var(--cpd-space-4x);
grid-template-rows: auto minmax(var(--cpd-space-4x), auto);
/* TODO: Support tabs */
grid-template-areas: ". title close ." "tabs tabs tabs tabs";
align-items: center;
}
.dialog .header h2 {
grid-area: title;
margin: 0;
}
.drawer .header {
grid-template-areas: "tabs";
position: relative;
}
.close {
grid-area: close;
}
.dialog .body {
padding-inline: var(--cpd-space-10x);
padding-block: var(--cpd-space-10x) var(--cpd-space-12x);
overflow: auto;
}
.drawer .body {
padding-inline: var(--cpd-space-4x);
padding-block: var(--cpd-space-9x) var(--cpd-space-10x);
}
.handle {
content: "";
position: absolute;
block-size: var(--handle-block-size);
inset-inline: calc((100% - var(--handle-inline-size)) / 2);
inset-block-start: var(--handle-inset-block-start);
background: var(--cpd-color-icon-secondary);
border-radius: var(--cpd-radius-pill-effect);
}

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2022 New Vector Ltd Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -14,123 +14,128 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/* eslint-disable jsx-a11y/no-autofocus */ import { ReactNode, useCallback } from "react";
import { useRef, useMemo, ReactNode } from "react";
import {
useOverlay,
usePreventScroll,
useModal,
OverlayContainer,
OverlayProps,
} from "@react-aria/overlays";
import {
OverlayTriggerState,
useOverlayTriggerState,
} from "@react-stately/overlays";
import { useDialog } from "@react-aria/dialog";
import { FocusScope } from "@react-aria/focus";
import { useButton } from "@react-aria/button";
import classNames from "classnames";
import { AriaDialogProps } from "@react-types/dialog"; import { AriaDialogProps } from "@react-types/dialog";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import {
Root as DialogRoot,
Portal as DialogPortal,
Overlay as DialogOverlay,
Content as DialogContent,
Title as DialogTitle,
Close as DialogClose,
} from "@radix-ui/react-dialog";
import { Drawer } from "vaul";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { ReactComponent as CloseIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
import classNames from "classnames";
import { Heading } from "@vector-im/compound-web";
import { ReactComponent as CloseIcon } from "./icons/Close.svg"; import styles from "./NewModal.module.css";
import styles from "./Modal.module.css"; import { useMediaQuery } from "./useMediaQuery";
import { Glass } from "./Glass";
export interface ModalProps extends OverlayProps, AriaDialogProps { // TODO: Support tabs
export interface ModalProps extends AriaDialogProps {
title: string; title: string;
children: ReactNode; children: ReactNode;
className?: string; className?: string;
mobileFullScreen?: boolean; /**
onClose: () => void; * The controlled open state of the modal.
*/
// An option to leave the open state uncontrolled is intentionally not
// provided, since modals are always opened due to external triggers, and it
// is the author's belief that controlled components lead to more obvious code.
open: boolean;
/**
* Callback for when the user dismisses the modal. If undefined, the modal
* will be non-dismissable.
*/
onDismiss?: () => void;
} }
/**
* A modal, taking the form of a drawer / bottom sheet on touchscreen devices,
* and a dialog box on desktop.
*/
export function Modal({ export function Modal({
title, title,
children, children,
className, className,
mobileFullScreen, open,
onClose, onDismiss,
...rest ...rest
}: ModalProps) { }: ModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const modalRef = useRef(null); const touchscreen = useMediaQuery("(hover: none)");
const { overlayProps, underlayProps } = useOverlay( const onOpenChange = useCallback(
{ ...rest, onClose }, (open: boolean) => {
modalRef if (!open) onDismiss?.();
);
usePreventScroll();
const { modalProps } = useModal();
const { dialogProps, titleProps } = useDialog(rest, modalRef);
const closeButtonRef = useRef(null);
const { buttonProps: closeButtonProps } = useButton(
{
onPress: () => onClose(),
}, },
closeButtonRef [onDismiss]
); );
return ( if (touchscreen) {
<OverlayContainer> return (
<div className={styles.modalOverlay} {...underlayProps}> <Drawer.Root
<FocusScope contain restoreFocus autoFocus> open={open}
<div onOpenChange={onOpenChange}
{...overlayProps} dismissible={onDismiss !== undefined}
{...dialogProps} >
{...modalProps} <Drawer.Portal>
ref={modalRef} <Drawer.Overlay className={styles.overlay} />
className={classNames( <Drawer.Content
styles.modal, className={classNames(className, styles.modal, styles.drawer)}
{ [styles.mobileFullScreen]: mobileFullScreen }, {...rest}
className
)}
> >
<div className={styles.modalHeader}> <div className={styles.content}>
<h3 {...titleProps}>{title}</h3> <div className={styles.header}>
<button <div className={styles.handle} />
{...closeButtonProps} <VisuallyHidden asChild>
ref={closeButtonRef} <Drawer.Title>{title}</Drawer.Title>
className={styles.closeButton} </VisuallyHidden>
data-testid="modal_close" </div>
title={t("Close")} <div className={styles.body}>{children}</div>
>
<CloseIcon />
</button>
</div> </div>
{children} </Drawer.Content>
</div> </Drawer.Portal>
</FocusScope> </Drawer.Root>
</div> );
</OverlayContainer> } else {
); return (
} <DialogRoot open={open} onOpenChange={onOpenChange}>
<DialogPortal>
interface ModalContentProps { <DialogOverlay
children: ReactNode; className={classNames(styles.overlay, styles.dialogOverlay)}
className?: string; />
} <DialogContent asChild {...rest}>
<Glass
export function ModalContent({ frosted
children, className={classNames(className, styles.modal, styles.dialog)}
className, >
...rest <div className={styles.content}>
}: ModalContentProps) { <div className={styles.header}>
return ( <DialogTitle asChild>
<div className={classNames(styles.content, className)} {...rest}> <Heading as="h2" weight="semibold" size="md">
{children} {title}
</div> </Heading>
); </DialogTitle>
} {onDismiss !== undefined && (
<DialogClose
export function useModalTriggerState(): { className={styles.close}
modalState: OverlayTriggerState; data-testid="modal_close"
modalProps: { isOpen: boolean; onClose: () => void }; aria-label={t("Close")}
} { >
const modalState = useOverlayTriggerState({}); <CloseIcon width={20} height={20} />
const modalProps = useMemo( </DialogClose>
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }), )}
[modalState] </div>
); <div className={styles.body}>{children}</div>
return { modalState, modalProps }; </div>
</Glass>
</DialogContent>
</DialogPortal>
</DialogRoot>
);
}
} }

38
src/Platform.ts Normal file
View File

@@ -0,0 +1,38 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* The platform on which the application is running.
*/
// The granularity of this value is kind of arbitrary: it distinguishes exactly
// the platforms that the app needs to know about in order to correctly
// implement the designs and work around platform-specific browser weirdness.
// Feel free to increase or decrease that granularity in the future as project
// requirements change.
export let platform: "android" | "ios" | "desktop";
if (/android/i.test(navigator.userAgent)) {
platform = "android";
// We include 'Mac' here and double-check for touch support because iPads on
// iOS 13 pretend to be a MacOS desktop
} else if (
/iPad|iPhone|iPod|Mac/.test(navigator.userAgent) &&
"ontouchend" in document
) {
platform = "ios";
} else {
platform = "desktop";
}

View File

@@ -19,7 +19,6 @@ import { useHistory, useLocation } from "react-router-dom";
import { useClientLegacy } from "./ClientContext"; import { useClientLegacy } from "./ClientContext";
import { useProfile } from "./profile/useProfile"; import { useProfile } from "./profile/useProfile";
import { useModalTriggerState } from "./Modal";
import { SettingsModal } from "./settings/SettingsModal"; import { SettingsModal } from "./settings/SettingsModal";
import { UserMenu } from "./UserMenu"; import { UserMenu } from "./UserMenu";
@@ -32,7 +31,11 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
const history = useHistory(); const history = useHistory();
const { client, logout, authenticated, passwordlessUser } = useClientLegacy(); const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
const { displayName, avatarUrl } = useProfile(client); const { displayName, avatarUrl } = useProfile(client);
const { modalState, modalProps } = useModalTriggerState(); const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const onDismissSettingsModal = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>(); const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>();
@@ -41,11 +44,11 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
switch (value) { switch (value) {
case "user": case "user":
setDefaultSettingsTab("profile"); setDefaultSettingsTab("profile");
modalState.open(); setSettingsModalOpen(true);
break; break;
case "settings": case "settings":
setDefaultSettingsTab("audio"); setDefaultSettingsTab("audio");
modalState.open(); setSettingsModalOpen(true);
break; break;
case "logout": case "logout":
logout?.(); logout?.();
@@ -55,7 +58,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
break; break;
} }
}, },
[history, location, logout, modalState] [history, location, logout, setSettingsModalOpen]
); );
const userName = client?.getUserIdLocalpart() ?? ""; const userName = client?.getUserIdLocalpart() ?? "";
@@ -70,11 +73,12 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
userId={client?.getUserId() ?? ""} userId={client?.getUserId() ?? ""}
displayName={displayName || (userName ? userName.replace("@", "") : "")} displayName={displayName || (userName ? userName.replace("@", "") : "")}
/> />
{modalState.isOpen && client && ( {client && (
<SettingsModal <SettingsModal
client={client} client={client}
defaultTab={defaultSettingsTab} defaultTab={defaultSettingsTab}
{...modalProps} open={settingsModalOpen}
onDismiss={onDismissSettingsModal}
/> />
)} )}
</> </>

View File

@@ -17,36 +17,29 @@ limitations under the License.
import { PressEvent } from "@react-types/shared"; import { PressEvent } from "@react-types/shared";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Modal, ModalContent } from "../Modal"; import { Modal } from "../Modal";
import { Button } from "../button"; import { Button } from "../button";
import { FieldRow } from "../input/Input"; import { FieldRow } from "../input/Input";
import styles from "./JoinExistingCallModal.module.css"; import styles from "./JoinExistingCallModal.module.css";
interface Props { interface Props {
open: boolean;
onDismiss: () => void;
onJoin: (e: PressEvent) => void; onJoin: (e: PressEvent) => void;
onClose: () => void;
// TODO: add used parameters for <Modal>
[index: string]: unknown;
} }
export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Modal <Modal title={t("Join existing call?")} open={open} onDismiss={onDismiss}>
title={t("Join existing call?")} <p>{t("This call already exists, would you like to join?")}</p>
isDismissable <FieldRow rightAlign className={styles.buttons}>
{...rest} <Button onPress={onDismiss}>{t("No")}</Button>
onClose={onClose} <Button onPress={onJoin} data-testid="home_joinExistingRoom">
> {t("Yes, join call")}
<ModalContent> </Button>
<p>{t("This call already exists, would you like to join?")}</p> </FieldRow>
<FieldRow rightAlign className={styles.buttons}>
<Button onPress={onClose}>{t("No")}</Button>
<Button onPress={onJoin} data-testid="home_joinExistingRoom">
{t("Yes, join call")}
</Button>
</FieldRow>
</ModalContent>
</Modal> </Modal>
); );
} }

View File

@@ -34,7 +34,6 @@ import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Button } from "../button"; import { Button } from "../button";
import { CallList } from "./CallList"; import { CallList } from "./CallList";
import { UserMenuContainer } from "../UserMenuContainer"; import { UserMenuContainer } from "../UserMenuContainer";
import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "./JoinExistingCallModal"; import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { Caption } from "../typography/Typography"; import { Caption } from "../typography/Typography";
import { Form } from "../form/Form"; import { Form } from "../form/Form";
@@ -54,7 +53,12 @@ export function RegisteredView({ client }: Props) {
const [optInAnalytics] = useOptInAnalytics(); const [optInAnalytics] = useOptInAnalytics();
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState(); const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
useState(false);
const onDismissJoinExistingCallModal = useCallback(
() => setJoinExistingCallModalOpen(false),
[setJoinExistingCallModalOpen]
);
const [e2eeEnabled] = useEnableE2EE(); const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback( const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
@@ -90,7 +94,7 @@ export function RegisteredView({ client }: Props) {
setExistingAlias(roomAliasLocalpartFromRoomName(roomName)); setExistingAlias(roomAliasLocalpartFromRoomName(roomName));
setLoading(false); setLoading(false);
setError(undefined); setError(undefined);
modalState.open(); setJoinExistingCallModalOpen(true);
} else { } else {
console.error(error); console.error(error);
setLoading(false); setLoading(false);
@@ -98,7 +102,7 @@ export function RegisteredView({ client }: Props) {
} }
}); });
}, },
[client, history, modalState, e2eeEnabled] [client, history, setJoinExistingCallModalOpen, e2eeEnabled]
); );
const recentRooms = useGroupCallRooms(client); const recentRooms = useGroupCallRooms(client);
@@ -164,9 +168,11 @@ export function RegisteredView({ client }: Props) {
)} )}
</main> </main>
</div> </div>
{modalState.isOpen && ( <JoinExistingCallModal
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} /> onJoin={onJoinExistingRoom}
)} open={joinExistingCallModalOpen}
onDismiss={onDismissJoinExistingCallModal}
/>
</> </>
); );
} }

View File

@@ -31,7 +31,6 @@ import {
sanitiseRoomNameInput, sanitiseRoomNameInput,
} from "../matrix-utils"; } from "../matrix-utils";
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration"; import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "./JoinExistingCallModal"; import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { useRecaptcha } from "../auth/useRecaptcha"; import { useRecaptcha } from "../auth/useRecaptcha";
import { Body, Caption, Link } from "../typography/Typography"; import { Body, Caption, Link } from "../typography/Typography";
@@ -54,7 +53,12 @@ export const UnauthenticatedView: FC = () => {
const { recaptchaKey, register } = useInteractiveRegistration(); const { recaptchaKey, register } = useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const { modalState, modalProps } = useModalTriggerState(); const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
useState(false);
const onDismissJoinExistingCallModal = useCallback(
() => setJoinExistingCallModalOpen(false),
[setJoinExistingCallModalOpen]
);
const [onFinished, setOnFinished] = useState<() => void>(); const [onFinished, setOnFinished] = useState<() => void>();
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -108,7 +112,7 @@ export const UnauthenticatedView: FC = () => {
}); });
setLoading(false); setLoading(false);
modalState.open(); setJoinExistingCallModalOpen(true);
return; return;
} else { } else {
throw error; throw error;
@@ -131,7 +135,15 @@ export const UnauthenticatedView: FC = () => {
reset(); reset();
}); });
}, },
[register, reset, execute, history, modalState, setClient, e2eeEnabled] [
register,
reset,
execute,
history,
setJoinExistingCallModalOpen,
setClient,
e2eeEnabled,
]
); );
return ( return (
@@ -221,8 +233,12 @@ export const UnauthenticatedView: FC = () => {
</Body> </Body>
</footer> </footer>
</div> </div>
{modalState.isOpen && onFinished && ( {onFinished && (
<JoinExistingCallModal onJoin={onFinished} {...modalProps} /> <JoinExistingCallModal
onJoin={onFinished}
open={joinExistingCallModalOpen}
onDismiss={onDismissJoinExistingCallModal}
/>
)} )}
</> </>
); );

View File

@@ -25,12 +25,6 @@ limitations under the License.
@import "@vector-im/compound-web/dist/style.css"; @import "@vector-im/compound-web/dist/style.css";
:root { :root {
--font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
--inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f,
U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF;
--font-scale: 1; --font-scale: 1;
--font-size-micro: calc(10px * var(--font-scale)); --font-size-micro: calc(10px * var(--font-scale));
--font-size-caption: calc(12px * var(--font-scale)); --font-size-caption: calc(12px * var(--font-scale));
@@ -149,7 +143,6 @@ body {
color: var(--cpd-color-text-primary); color: var(--cpd-color-text-primary);
color-scheme: dark; color-scheme: dark;
margin: 0; margin: 0;
font-family: var(--font-family);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
@@ -157,7 +150,9 @@ body {
html, html,
body, body,
#root { #root {
height: 100%; /* We use !important here to override vaul drawers, which have a side effect
of setting height: auto; on the body element and messing up our layouts */
height: 100% !important;
} }
#root { #root {
@@ -165,6 +160,21 @@ body,
flex-direction: column; flex-direction: column;
} }
/* On Android and iOS, prefer native system fonts. The global.css file of
Compound Web is where these variables ultimately get consumed to set the page's
font-family. */
body[data-platform="android"] {
--cpd-font-family-sans: "Roboto", "Noto", "Inter", sans-serif;
}
body[data-platform="ios"] {
--cpd-font-family-sans: -apple-system, BlinkMacSystemFont, "Inter", sans-serif;
}
body[data-platform="desktop"] {
--cpd-font-family-sans: "Inter", sans-serif;
}
h1, h1,
h2, h2,
h3, h3,

View File

@@ -24,6 +24,7 @@ import * as Sentry from "@sentry/react";
import { getUrlParams } from "./UrlParams"; import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
import { ElementCallOpenTelemetry } from "./otel/otel"; import { ElementCallOpenTelemetry } from "./otel/otel";
import { platform } from "./Platform";
enum LoadState { enum LoadState {
None, None,
@@ -107,6 +108,9 @@ export class Initializer {
fonts.map((f) => `"${f}"`).join(", ") fonts.map((f) => `"${f}"`).join(", ")
); );
} }
// Add the platform to the DOM, so CSS can query it
document.body.setAttribute("data-platform", platform);
} }
public static init(): Promise<void> | null { public static init(): Promise<void> | null {

View File

@@ -200,8 +200,10 @@ export const useMediaDevices = () => useContext(MediaDevicesContext);
* default because it may involve requesting additional permissions from the * default because it may involve requesting additional permissions from the
* user. * user.
*/ */
export const useMediaDeviceNames = (context: MediaDevices) => export const useMediaDeviceNames = (context: MediaDevices, enabled = true) =>
useEffect(() => { useEffect(() => {
context.startUsingDeviceNames(); if (enabled) {
return context.stopUsingDeviceNames; context.startUsingDeviceNames();
}, [context]); return context.stopUsingDeviceNames;
}
}, [context, enabled]);

View File

@@ -45,7 +45,6 @@ import {
import { useEnableE2EE } from "../settings/useSetting"; import { useEnableE2EE } from "../settings/useSetting";
import { useRoomAvatar } from "./useRoomAvatar"; import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName"; import { useRoomName } from "./useRoomName";
import { useModalTriggerState } from "../Modal";
import { useJoinRule } from "./useJoinRule"; import { useJoinRule } from "./useJoinRule";
import { ShareModal } from "./ShareModal"; import { ShareModal } from "./ShareModal";
@@ -286,12 +285,15 @@ export function GroupCallView({
const joinRule = useJoinRule(rtcSession.room); const joinRule = useJoinRule(rtcSession.room);
const { modalState: shareModalState, modalProps: shareModalProps } = const [shareModalOpen, setShareModalOpen] = useState(false);
useModalTriggerState(); const onDismissShareModal = useCallback(
() => setShareModalOpen(false),
[setShareModalOpen]
);
const onShareClickFn = useCallback( const onShareClickFn = useCallback(
() => shareModalState.open(), () => setShareModalOpen(true),
[shareModalState] [setShareModalOpen]
); );
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null; const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
@@ -311,8 +313,12 @@ export function GroupCallView({
return <ErrorView error={new Error("You need to enable E2EE to join.")} />; return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
} }
const shareModal = shareModalState.isOpen && ( const shareModal = (
<ShareModal roomId={rtcSession.room.roomId} {...shareModalProps} /> <ShareModal
roomId={rtcSession.room.roomId}
open={shareModalOpen}
onDismiss={onDismissShareModal}
/>
); );
if (isJoined) { if (isJoined) {

View File

@@ -30,7 +30,6 @@ import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { OverlayTriggerState } from "@react-stately/overlays";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
@@ -51,7 +50,6 @@ import {
VideoGrid, VideoGrid,
} from "../video-grid/VideoGrid"; } from "../video-grid/VideoGrid";
import { useShowConnectionStats } from "../settings/useSetting"; import { useShowConnectionStats } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useUrlParams } from "../UrlParams"; import { useUrlParams } from "../UrlParams";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
@@ -313,25 +311,20 @@ export function InCallView({
); );
}; };
const { const rageshakeRequestModalProps = useRageshakeRequestModal(
modalState: rageshakeRequestModalState, rtcSession.room.roomId
modalProps: rageshakeRequestModalProps, );
} = useRageshakeRequestModal(rtcSession.room.roomId);
const { const [settingsModalOpen, setSettingsModalOpen] = useState(false);
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const openSettings = useCallback(() => { const openSettings = useCallback(
settingsModalState.open(); () => setSettingsModalOpen(true),
}, [settingsModalState]); [setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const toggleScreensharing = useCallback(async () => { const toggleScreensharing = useCallback(async () => {
exitFullscreen(); exitFullscreen();
@@ -442,19 +435,13 @@ export function InCallView({
show={showInspector} show={showInspector}
/> />
)*/} )*/}
{rageshakeRequestModalState.isOpen && !noControls && ( {!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
<RageshakeRequestModal <SettingsModal
{...rageshakeRequestModalProps} client={client}
roomId={rtcSession.room.roomId} roomId={rtcSession.room.roomId}
/> open={settingsModalOpen}
)} onDismiss={closeSettings}
{settingsModalState.isOpen && ( />
<SettingsModal
client={client}
roomId={rtcSession.room.roomId}
{...settingsModalProps}
/>
)}
</div> </div>
); );
} }

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal"; import { Modal, ModalProps } from "../Modal";
import { Button } from "../button"; import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input"; import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake"; import { useSubmitRageshake } from "../settings/submit-rageshake";
@@ -26,51 +26,49 @@ import { Body } from "../typography/Typography";
interface Props extends Omit<ModalProps, "title" | "children"> { interface Props extends Omit<ModalProps, "title" | "children"> {
rageshakeRequestId: string; rageshakeRequestId: string;
roomId: string; roomId: string;
onClose: () => void; open: boolean;
onDismiss: () => void;
} }
export const RageshakeRequestModal: FC<Props> = ({ export const RageshakeRequestModal: FC<Props> = ({
rageshakeRequestId, rageshakeRequestId,
roomId, roomId,
...rest open,
onDismiss,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake(); const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => { useEffect(() => {
if (sent) { if (sent) onDismiss();
rest.onClose(); }, [sent, onDismiss]);
}
}, [sent, rest]);
return ( return (
<Modal title={t("Debug log request")} isDismissable {...rest}> <Modal title={t("Debug log request")} open={open} onDismiss={onDismiss}>
<ModalContent> <Body>
<Body> {t(
{t( "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log."
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log."
)}
</Body>
<FieldRow>
<Button
onPress={() =>
submitRageshake({
sendLogs: true,
rageshakeRequestId,
roomId,
})
}
disabled={sending}
>
{sending ? t("Sending debug logs…") : t("Send debug logs")}
</Button>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage error={error} />
</FieldRow>
)} )}
</ModalContent> </Body>
<FieldRow>
<Button
onPress={() =>
submitRageshake({
sendLogs: true,
rageshakeRequestId,
roomId,
})
}
disabled={sending}
>
{sending ? t("Sending debug logs…") : t("Send debug logs")}
</Button>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage error={error} />
</FieldRow>
)}
</Modal> </Modal>
); );
}; };

View File

@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.inviteModal {
max-width: 413px;
}
.copyButton { .copyButton {
width: 100%; width: 100%;
} }

View File

@@ -17,35 +17,30 @@ limitations under the License.
import { FC } from "react"; import { FC } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal"; import { Modal } from "../Modal";
import { CopyButton } from "../button"; import { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils"; import { getRoomUrl } from "../matrix-utils";
import styles from "./ShareModal.module.css"; import styles from "./ShareModal.module.css";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement"; import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
interface Props extends Omit<ModalProps, "title" | "children"> { interface Props {
roomId: string; roomId: string;
open: boolean;
onDismiss: () => void;
} }
export const ShareModal: FC<Props> = ({ roomId, ...rest }) => { export const ShareModal: FC<Props> = ({ roomId, open, onDismiss }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(roomId); const roomSharedKey = useRoomSharedKey(roomId);
return ( return (
<Modal <Modal title={t("Share this call")} open={open} onDismiss={onDismiss}>
title={t("Share this call")} <p>{t("Copy and share this call link")}</p>
isDismissable <CopyButton
className={styles.inviteModal} className={styles.copyButton}
{...rest} value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
> data-testid="modal_inviteLink"
<ModalContent> />
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
data-testid="modal_inviteLink"
/>
</ModalContent>
</Modal> </Modal>
); );
}; };

View File

@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useEffect, useCallback, useMemo, useRef, FC } from "react"; import { useEffect, useCallback, useMemo, useRef, FC, useState } from "react";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer"; import { ResizeObserver } from "@juggle/resize-observer";
import { OverlayTriggerState } from "@react-stately/overlays";
import { usePreviewTracks } from "@livekit/components-react"; import { usePreviewTracks } from "@livekit/components-react";
import { import {
CreateLocalTracksOptions, CreateLocalTracksOptions,
@@ -28,7 +27,6 @@ import {
import { MicButton, SettingsButton, VideoButton } from "../button"; import { MicButton, SettingsButton, VideoButton } from "../button";
import { Avatar } from "../Avatar"; import { Avatar } from "../Avatar";
import styles from "./VideoPreview.module.css"; import styles from "./VideoPreview.module.css";
import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal"; import { SettingsModal } from "../settings/SettingsModal";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";
import { useMediaDevices } from "../livekit/MediaDevicesContext"; import { useMediaDevices } from "../livekit/MediaDevicesContext";
@@ -54,20 +52,16 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
const { client } = useClient(); const { client } = useClient();
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver }); const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const { const [settingsModalOpen, setSettingsModalOpen] = useState(false);
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const openSettings = useCallback(() => { const openSettings = useCallback(
settingsModalState.open(); () => setSettingsModalOpen(true),
}, [settingsModalState]); [setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const devices = useMediaDevices(); const devices = useMediaDevices();
@@ -153,8 +147,12 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
<SettingsButton onPress={openSettings} /> <SettingsButton onPress={openSettings} />
</div> </div>
</> </>
{settingsModalState.isOpen && client && ( {client && (
<SettingsModal client={client} {...settingsModalProps} /> <SettingsModal
client={client}
open={settingsModalOpen}
onDismiss={closeSettings}
/>
)} )}
</div> </div>
); );

View File

@@ -16,21 +16,7 @@ limitations under the License.
import { useEffect } from "react"; import { useEffect } from "react";
// https://stackoverflow.com/a/9039885 import { platform } from "../Platform";
function isIOS() {
return (
[
"iPad Simulator",
"iPhone Simulator",
"iPod Simulator",
"iPad",
"iPhone",
"iPod",
].includes(navigator.platform) ||
// iPad on iOS 13 detection
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
);
}
export function usePageUnload(callback: () => void) { export function usePageUnload(callback: () => void) {
useEffect(() => { useEffect(() => {
@@ -53,7 +39,7 @@ export function usePageUnload(callback: () => void) {
} }
// iOS doesn't fire beforeunload event, so leave the call when you hide the page. // iOS doesn't fire beforeunload event, so leave the call when you hide the page.
if (isIOS()) { if (platform === "ios") {
window.addEventListener("pagehide", onBeforeUnload); window.addEventListener("pagehide", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2022 New Vector Ltd Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -15,18 +15,13 @@ limitations under the License.
*/ */
.settingsModal { .settingsModal {
width: 774px; block-size: 550px;
height: 480px;
} }
.settingsModal p { .settingsModal p {
color: var(--cpd-color-text-secondary); color: var(--cpd-color-text-secondary);
} }
.tabContainer {
padding: 27px 20px;
}
.fieldRowText { .fieldRowText {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@@ -51,11 +51,11 @@ import {
} from "../livekit/MediaDevicesContext"; } from "../livekit/MediaDevicesContext";
interface Props { interface Props {
isOpen: boolean; open: boolean;
onDismiss: () => void;
client: MatrixClient; client: MatrixClient;
roomId?: string; roomId?: string;
defaultTab?: string; defaultTab?: string;
onClose: () => void;
} }
export const SettingsModal = (props: Props) => { export const SettingsModal = (props: Props) => {
@@ -119,7 +119,7 @@ export const SettingsModal = (props: Props) => {
); );
const devices = useMediaDevices(); const devices = useMediaDevices();
useMediaDeviceNames(devices); useMediaDeviceNames(devices, props.open);
const audioTab = ( const audioTab = (
<TabItem <TabItem
@@ -289,10 +289,9 @@ export const SettingsModal = (props: Props) => {
return ( return (
<Modal <Modal
title={t("Settings")} title={t("Settings")}
isDismissable
mobileFullScreen
className={styles.settingsModal} className={styles.settingsModal}
{...props} open={props.open}
onDismiss={props.onDismiss}
> >
<TabContainer <TabContainer
onSelectionChange={onSelectedTabChanged} onSelectionChange={onSelectedTabChanged}

View File

@@ -14,20 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useContext, useEffect, useState } from "react"; import {
ComponentProps,
useCallback,
useContext,
useEffect,
useState,
} from "react";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import pako from "pako"; import pako from "pako";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { OverlayTriggerState } from "@react-stately/overlays";
import { ClientEvent } from "matrix-js-sdk/src/client"; import { ClientEvent } from "matrix-js-sdk/src/client";
import { getLogsForReport } from "./rageshake"; import { getLogsForReport } from "./rageshake";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";
import { InspectorContext } from "../room/GroupCallInspector"; import { InspectorContext } from "../room/GroupCallInspector";
import { useModalTriggerState } from "../Modal";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { ElementCallOpenTelemetry } from "../otel/otel"; import { ElementCallOpenTelemetry } from "../otel/otel";
import { RageshakeRequestModal } from "../room/RageshakeRequestModal";
const gzip = (text: string): Blob => { const gzip = (text: string): Blob => {
// encode as UTF-8 // encode as UTF-8
@@ -343,22 +348,12 @@ export function useRageshakeRequest(): (
return sendRageshakeRequest; return sendRageshakeRequest;
} }
interface ModalProps {
isOpen: boolean;
onClose: () => void;
}
interface ModalPropsWithId extends ModalProps {
rageshakeRequestId: string;
}
export function useRageshakeRequestModal(roomId: string): { export function useRageshakeRequestModal(
modalState: OverlayTriggerState; roomId: string
modalProps: ModalPropsWithId; ): ComponentProps<typeof RageshakeRequestModal> {
} { const [open, setOpen] = useState(false);
const { modalState, modalProps } = useModalTriggerState() as { const onDismiss = useCallback(() => setOpen(false), [setOpen]);
modalState: OverlayTriggerState;
modalProps: ModalProps;
};
const { client } = useClient(); const { client } = useClient();
const [rageshakeRequestId, setRageshakeRequestId] = useState<string>(); const [rageshakeRequestId, setRageshakeRequestId] = useState<string>();
@@ -374,7 +369,7 @@ export function useRageshakeRequestModal(roomId: string): {
client.getUserId() !== event.getSender() client.getUserId() !== event.getSender()
) { ) {
setRageshakeRequestId(event.getContent().request_id); setRageshakeRequestId(event.getContent().request_id);
modalState.open(); setOpen(true);
} }
}; };
@@ -383,10 +378,12 @@ export function useRageshakeRequestModal(roomId: string): {
return () => { return () => {
client.removeListener(ClientEvent.Event, onEvent); client.removeListener(ClientEvent.Event, onEvent);
}; };
}, [modalState.open, roomId, client, modalState]); }, [setOpen, roomId, client]);
return { return {
modalState, rageshakeRequestId: rageshakeRequestId ?? "",
modalProps: { ...modalProps, rageshakeRequestId: rageshakeRequestId ?? "" }, roomId,
open,
onDismiss,
}; };
} }

View File

@@ -78,32 +78,3 @@ limitations under the License.
padding: 0; padding: 0;
overflow-y: auto; overflow-y: auto;
} }
@media (min-width: 800px) {
.tab {
width: 200px;
padding: 0 16px;
}
.tab > * {
margin: 0 12px 0 0;
}
.tabContainer {
width: 100%;
flex-direction: row;
padding: 20px 18px;
box-sizing: border-box;
overflow: hidden;
}
.tabList {
flex-direction: column;
margin-bottom: 0;
gap: 0;
}
.tabPanel {
padding: 0 40px;
}
}

View File

@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ComponentProps, forwardRef, useCallback, useEffect } from "react"; import {
ComponentProps,
forwardRef,
useCallback,
useEffect,
useState,
} from "react";
import { animated } from "@react-spring/web"; import { animated } from "@react-spring/web";
import classNames from "classnames"; import classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -36,7 +42,6 @@ import { Avatar } from "../Avatar";
import styles from "./VideoTile.module.css"; import styles from "./VideoTile.module.css";
import { useReactiveState } from "../useReactiveState"; import { useReactiveState } from "../useReactiveState";
import { AudioButton, FullscreenButton } from "../button/Button"; import { AudioButton, FullscreenButton } from "../button/Button";
import { useModalTriggerState } from "../Modal";
import { VideoTileSettingsModal } from "./VideoTileSettingsModal"; import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
export interface ItemData { export interface ItemData {
@@ -117,11 +122,16 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
onToggleFullscreen(data.id); onToggleFullscreen(data.id);
}, [data, onToggleFullscreen]); }, [data, onToggleFullscreen]);
const { const [videoTileSettingsModalOpen, setVideoTileSettingsModalOpen] =
modalState: videoTileSettingsModalState, useState(false);
modalProps: videoTileSettingsModalProps, const openVideoTileSettingsModal = useCallback(
} = useModalTriggerState(); () => setVideoTileSettingsModalOpen(true),
const onOptionsPress = videoTileSettingsModalState.open; [setVideoTileSettingsModalOpen]
);
const closeVideoTileSettingsModal = useCallback(
() => setVideoTileSettingsModalOpen(false),
[setVideoTileSettingsModalOpen]
);
const toolbarButtons: JSX.Element[] = []; const toolbarButtons: JSX.Element[] = [];
if (!sfuParticipant.isLocal) { if (!sfuParticipant.isLocal) {
@@ -130,7 +140,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
key="localVolume" key="localVolume"
className={styles.button} className={styles.button}
volume={(sfuParticipant as RemoteParticipant).getVolume() ?? 0} volume={(sfuParticipant as RemoteParticipant).getVolume() ?? 0}
onPress={onOptionsPress} onPress={openVideoTileSettingsModal}
/> />
); );
@@ -208,10 +218,11 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
: Track.Source.ScreenShare : Track.Source.ScreenShare
} }
/> />
{videoTileSettingsModalState.isOpen && !maximised && ( {!maximised && (
<VideoTileSettingsModal <VideoTileSettingsModal
{...videoTileSettingsModalProps}
data={data} data={data}
open={videoTileSettingsModalOpen}
onDismiss={closeVideoTileSettingsModal}
/> />
)} )}
</animated.div> </animated.div>

View File

@@ -66,23 +66,21 @@ const LocalVolume: React.FC<LocalVolumeProps> = ({
); );
}; };
// TODO: Extend ModalProps
interface Props { interface Props {
data: ItemData; data: ItemData;
onClose: () => void; open: boolean;
onDismiss: () => void;
} }
export const VideoTileSettingsModal = ({ data, onClose, ...rest }: Props) => { export const VideoTileSettingsModal = ({ data, open, onDismiss }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Modal <Modal
className={styles.videoTileSettingsModal} className={styles.videoTileSettingsModal}
title={t("Local volume")} title={t("Local volume")}
isDismissable open={open}
mobileFullScreen onDismiss={onDismiss}
onClose={onClose}
{...rest}
> >
<div className={styles.content}> <div className={styles.content}>
<LocalVolume <LocalVolume

1995
yarn.lock

File diff suppressed because it is too large Load Diff